Files
zed/crates/multi_buffer/src/path_key.rs
T
f8e05120fd git: Rework side-by-side diff to use distinct buffers for the left-hand side (#44838)
This PR reworks the (still feature-gated) side-by-side diff view to use
a different approach to representing the multibuffers on the left- and
right-hand sides.

Previously, these two multibuffers used identical sets of buffers and
excerpts, and were made to behave differently by adding a new knob to
the multibuffer controlling how diffs are displayed. Specifically, the
left-hand side multibuffer would filter out the added range of each hunk
from the excerpts using a new `FilteredInsertedHunk` diff transform, and
the right-hand side would simply not show the deleted sides of expanded
hunks. This approach has some problems:

- Line numbers, and actions that navigate by line number, behaved
incorrectly for the left-hand side.
- Syntax highlighting and other features that use the buffer syntax tree
also behaved incorrectly for the left-hand side.

In this PR, we've switched to using independent buffers to build the
left-hand side. These buffers are constructed using the base texts for
the corresponding diffs, and their lifecycle is managed by `BufferDiff`.
The red "deleted" regions on the left-hand side are represented by
`BufferContent` diff transforms, not `DeletedHunk` transforms. This
means each excerpt on the left represents a contiguous slice of a single
buffer, which fixes the above issues by construction.

The tradeoff with this new approach is that we now have to manually
synchronize excerpt ranges from the right side to the left, which we do
using `BufferDiffSnapshot::row_to_base_text_row`.

Release Notes:

- N/A

---------

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Co-authored-by: HactarCE <6060305+HactarCE@users.noreply.github.com>
Co-authored-by: Miguel Raz Guzmán Macedo <miguel@zed.dev>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Cameron <cameron@zed.dev>
2025-12-31 20:02:45 -05:00

449 lines
17 KiB
Rust

use std::{mem, ops::Range, sync::Arc};
use collections::HashSet;
use gpui::{App, AppContext, Context, Entity};
use itertools::Itertools;
use language::{Buffer, BufferSnapshot};
use rope::Point;
use text::{Bias, BufferId, OffsetRangeExt, locator::Locator};
use util::{post_inc, rel_path::RelPath};
use ztracing::instrument;
use crate::{
Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, build_excerpt_ranges,
};
#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Hash, Debug)]
pub struct PathKey {
// Used by the derived PartialOrd & Ord
pub sort_prefix: Option<u64>,
pub path: Arc<RelPath>,
}
impl PathKey {
pub fn with_sort_prefix(sort_prefix: u64, path: Arc<RelPath>) -> Self {
Self {
sort_prefix: Some(sort_prefix),
path,
}
}
pub fn for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Self {
if let Some(file) = buffer.read(cx).file() {
Self::with_sort_prefix(file.worktree_id(cx).to_proto(), file.path().clone())
} else {
Self {
sort_prefix: None,
path: RelPath::unix(&buffer.entity_id().to_string())
.unwrap()
.into_arc(),
}
}
}
}
impl MultiBuffer {
pub fn paths(&self) -> impl Iterator<Item = &PathKey> + '_ {
self.excerpts_by_path.keys()
}
pub fn excerpts_for_path(&self, path: &PathKey) -> impl '_ + Iterator<Item = ExcerptId> {
self.excerpts_by_path
.get(path)
.map(|excerpts| excerpts.as_slice())
.unwrap_or(&[])
.iter()
.copied()
}
pub fn path_for_excerpt(&self, excerpt: ExcerptId) -> Option<PathKey> {
self.paths_by_excerpt.get(&excerpt).cloned()
}
pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
if let Some(to_remove) = self.excerpts_by_path.remove(&path) {
self.remove_excerpts(to_remove, cx)
}
}
pub fn buffer_for_path(&self, path: &PathKey, cx: &App) -> Option<Entity<Buffer>> {
let excerpt_id = self.excerpts_by_path.get(path)?.first()?;
let snapshot = self.read(cx);
let excerpt = snapshot.excerpt(*excerpt_id)?;
self.buffer(excerpt.buffer_id)
}
pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option<Anchor> {
let excerpt_id = self.excerpts_by_path.get(path)?.first()?;
let snapshot = self.read(cx);
let excerpt = snapshot.excerpt(*excerpt_id)?;
Some(Anchor::in_buffer(excerpt.id, excerpt.range.context.start))
}
/// Sets excerpts, returns `true` if at least one new excerpt was added.
#[instrument(skip_all)]
pub fn set_excerpts_for_path(
&mut self,
path: PathKey,
buffer: Entity<Buffer>,
ranges: impl IntoIterator<Item = Range<Point>>,
context_line_count: u32,
cx: &mut Context<Self>,
) -> (Vec<Range<Anchor>>, bool) {
let buffer_snapshot = buffer.read(cx).snapshot();
let excerpt_ranges = build_excerpt_ranges(ranges, context_line_count, &buffer_snapshot);
let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges);
self.set_merged_excerpt_ranges_for_path(
path,
buffer,
excerpt_ranges,
&buffer_snapshot,
new,
counts,
cx,
)
}
pub fn set_excerpt_ranges_for_path(
&mut self,
path: PathKey,
buffer: Entity<Buffer>,
buffer_snapshot: &BufferSnapshot,
excerpt_ranges: Vec<ExcerptRange<Point>>,
cx: &mut Context<Self>,
) -> (Vec<Range<Anchor>>, bool) {
let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges);
self.set_merged_excerpt_ranges_for_path(
path,
buffer,
excerpt_ranges,
buffer_snapshot,
new,
counts,
cx,
)
}
pub fn set_anchored_excerpts_for_path(
&self,
path_key: PathKey,
buffer: Entity<Buffer>,
ranges: Vec<Range<text::Anchor>>,
context_line_count: u32,
cx: &Context<Self>,
) -> impl Future<Output = Vec<Range<Anchor>>> + use<> {
let buffer_snapshot = buffer.read(cx).snapshot();
let multi_buffer = cx.weak_entity();
let mut app = cx.to_async();
async move {
let snapshot = buffer_snapshot.clone();
let (excerpt_ranges, new, counts) = app
.background_spawn(async move {
let ranges = ranges.into_iter().map(|range| range.to_point(&snapshot));
let excerpt_ranges =
build_excerpt_ranges(ranges, context_line_count, &snapshot);
let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges);
(excerpt_ranges, new, counts)
})
.await;
multi_buffer
.update(&mut app, move |multi_buffer, cx| {
let (ranges, _) = multi_buffer.set_merged_excerpt_ranges_for_path(
path_key,
buffer,
excerpt_ranges,
&buffer_snapshot,
new,
counts,
cx,
);
ranges
})
.ok()
.unwrap_or_default()
}
}
pub fn remove_excerpts_for_buffer(&mut self, buffer: BufferId, cx: &mut Context<Self>) {
self.remove_excerpts(
self.excerpts_for_buffer(buffer, cx)
.into_iter()
.map(|(excerpt, _)| excerpt),
cx,
);
}
pub(super) 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()
.filter_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 mut excerpt_id_ = None;
let expanded_ranges = excerpt_ids.iter().filter_map(|excerpt_id| {
let excerpt = snapshot.excerpt(*excerpt_id)?;
let excerpt_id = excerpt.id;
if excerpt_id_.is_none() {
excerpt_id_ = Some(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.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()
&& last_range.context.end >= range.context.start
{
last_range.context.end = range.context.end;
continue;
}
merged_ranges.push(range)
}
let Some(excerpt_id) = excerpt_id_ else {
continue;
};
let Some(buffer_id) = &snapshot.buffer_id_for_excerpt(excerpt_id) else {
continue;
};
let Some(buffer) = self.buffers.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.
fn set_merged_excerpt_ranges_for_path(
&mut self,
path: PathKey,
buffer: Entity<Buffer>,
ranges: Vec<ExcerptRange<Point>>,
buffer_snapshot: &BufferSnapshot,
new: Vec<ExcerptRange<Point>>,
counts: Vec<usize>,
cx: &mut Context<Self>,
) -> (Vec<Range<Anchor>>, bool) {
let (excerpt_ids, added_a_new_excerpt) =
self.update_path_excerpts(path, buffer, buffer_snapshot, new, cx);
let mut result = Vec::new();
let mut ranges = ranges.into_iter();
for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(counts.into_iter()) {
for range in ranges.by_ref().take(range_count) {
let range = Anchor::range_in_buffer(
excerpt_id,
buffer_snapshot.anchor_before(&range.primary.start)
..buffer_snapshot.anchor_after(&range.primary.end),
);
result.push(range)
}
}
(result, added_a_new_excerpt)
}
pub fn update_path_excerpts(
&mut self,
path: PathKey,
buffer: Entity<Buffer>,
buffer_snapshot: &BufferSnapshot,
new: Vec<ExcerptRange<Point>>,
cx: &mut Context<Self>,
) -> (Vec<ExcerptId>, bool) {
let mut insert_after = self
.excerpts_by_path
.range(..path.clone())
.next_back()
.and_then(|(_, value)| value.last().copied())
.unwrap_or(ExcerptId::min());
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();
let mut excerpt_ids = Vec::new();
let mut to_remove = Vec::new();
let mut to_insert: Vec<(ExcerptId, ExcerptRange<Point>)> = Vec::new();
let mut added_a_new_excerpt = false;
let snapshot = self.snapshot(cx);
let mut next_excerpt_id =
// todo(lw): is this right? What if we remove the last excerpt, then we might reallocate with a wrong mapping?
if let Some(last_entry) = self.snapshot.borrow().excerpt_ids.last() {
last_entry.id.0 + 1
} else {
1
};
let mut next_excerpt_id = move || ExcerptId(post_inc(&mut next_excerpt_id));
let mut excerpts_cursor = snapshot.excerpts.cursor::<Option<&Locator>>(());
excerpts_cursor.next();
loop {
let existing = if let Some(&existing_id) = existing_iter.peek() {
let locator = snapshot.excerpt_locator_for_id(existing_id);
excerpts_cursor.seek_forward(&Some(locator), Bias::Left);
if let Some(excerpt) = excerpts_cursor.item() {
if excerpt.buffer_id != buffer_snapshot.remote_id() {
to_remove.push(existing_id);
existing_iter.next();
continue;
}
Some((existing_id, excerpt.range.context.to_point(buffer_snapshot)))
} else {
None
}
} else {
None
};
let new = new_iter.peek();
if let Some((last_id, last)) = to_insert.last_mut() {
if let Some(new) = new
&& last.context.end >= new.context.start
{
last.context.end = last.context.end.max(new.context.end);
excerpt_ids.push(*last_id);
new_iter.next();
continue;
}
if let Some((existing_id, existing_range)) = &existing
&& last.context.end >= existing_range.start
{
last.context.end = last.context.end.max(existing_range.end);
to_remove.push(*existing_id);
self.snapshot
.get_mut()
.replaced_excerpts
.insert(*existing_id, *last_id);
existing_iter.next();
continue;
}
}
match (new, existing) {
(None, None) => break,
(None, Some((existing_id, _))) => {
existing_iter.next();
to_remove.push(existing_id);
continue;
}
(Some(_), None) => {
added_a_new_excerpt = true;
let new_id = next_excerpt_id();
excerpt_ids.push(new_id);
to_insert.push((new_id, new_iter.next().unwrap()));
continue;
}
(Some(new), Some((_, existing_range))) => {
if existing_range.end < new.context.start {
let existing_id = existing_iter.next().unwrap();
to_remove.push(existing_id);
continue;
} else if existing_range.start > new.context.end {
let new_id = next_excerpt_id();
excerpt_ids.push(new_id);
to_insert.push((new_id, new_iter.next().unwrap()));
continue;
}
if existing_range.start == new.context.start
&& existing_range.end == new.context.end
{
self.insert_excerpts_with_ids_after(
insert_after,
buffer.clone(),
mem::take(&mut to_insert),
cx,
);
insert_after = existing_iter.next().unwrap();
excerpt_ids.push(insert_after);
new_iter.next();
} else {
let existing_id = existing_iter.next().unwrap();
let new_id = next_excerpt_id();
self.snapshot
.get_mut()
.replaced_excerpts
.insert(existing_id, new_id);
to_remove.push(existing_id);
let mut range = new_iter.next().unwrap();
range.context.start = range.context.start.min(existing_range.start);
range.context.end = range.context.end.max(existing_range.end);
excerpt_ids.push(new_id);
to_insert.push((new_id, range));
}
}
};
}
self.insert_excerpts_with_ids_after(insert_after, buffer, to_insert, cx);
// todo(lw): There is a logic bug somewhere that causes the to_remove vector to be not ordered correctly
to_remove.sort_by_cached_key(|&id| snapshot.excerpt_locator_for_id(id));
self.remove_excerpts(to_remove, cx);
if excerpt_ids.is_empty() {
self.excerpts_by_path.remove(&path);
} else {
for excerpt_id in &excerpt_ids {
self.paths_by_excerpt.insert(*excerpt_id, path.clone());
}
let snapshot = &*self.snapshot.get_mut();
let mut excerpt_ids: Vec<_> = excerpt_ids.iter().dedup().cloned().collect();
excerpt_ids.sort_by_cached_key(|&id| snapshot.excerpt_locator_for_id(id));
self.excerpts_by_path.insert(path, excerpt_ids);
}
(excerpt_ids, added_a_new_excerpt)
}
}