Project Diff 2 (#23891)

This adds a new version of the project diff editor to go alongside the
new git panel.

The basics seem to be working, but still todo:

* [ ] Fix untracked files
* [ ] Fix deleted files
* [ ] Show commit message editor at top
* [x] Handle empty state
* [x] Fix panic where locator sometimes seeks to wrong excerpt

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2025-02-03 13:18:50 -07:00 committed by GitHub
parent 27a413a5e3
commit 45708d2680
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1023 additions and 125 deletions

View file

@ -35,6 +35,7 @@ use std::{
iter::{self, FromIterator},
mem,
ops::{Range, RangeBounds, Sub},
path::Path,
str,
sync::Arc,
time::{Duration, Instant},
@ -65,6 +66,8 @@ pub struct MultiBuffer {
snapshot: RefCell<MultiBufferSnapshot>,
/// 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<Arc<Path>, Vec<ExcerptId>>,
diff_bases: HashMap<BufferId, ChangeSetState>,
all_diff_hunks_expanded: bool,
subscriptions: Topic,
@ -494,6 +497,7 @@ impl MultiBuffer {
singleton: false,
capability,
title: None,
buffers_by_path: Default::default(),
history: History {
next_transaction_id: clock::Lamport::default(),
undo_stack: Vec::new(),
@ -508,6 +512,7 @@ impl MultiBuffer {
Self {
snapshot: Default::default(),
buffers: Default::default(),
buffers_by_path: Default::default(),
diff_bases: HashMap::default(),
all_diff_hunks_expanded: false,
subscriptions: Default::default(),
@ -561,6 +566,7 @@ impl MultiBuffer {
Self {
snapshot: RefCell::new(self.snapshot.borrow().clone()),
buffers: RefCell::new(buffers),
buffers_by_path: Default::default(),
diff_bases,
all_diff_hunks_expanded: self.all_diff_hunks_expanded,
subscriptions: Default::default(),
@ -648,8 +654,8 @@ impl MultiBuffer {
self.read(cx).len()
}
pub fn is_empty(&self, cx: &App) -> bool {
self.len(cx) != 0
pub fn is_empty(&self) -> bool {
self.buffers.borrow().is_empty()
}
pub fn symbols_containing<T: ToOffset>(
@ -1388,6 +1394,138 @@ impl MultiBuffer {
anchor_ranges
}
pub fn location_for_path(&self, path: &Arc<Path>, cx: &App) -> Option<Anchor> {
let excerpt_id = self.buffers_by_path.get(path)?.first()?;
let snapshot = self.snapshot(cx);
let excerpt = snapshot.excerpt(*excerpt_id)?;
Some(Anchor::in_buffer(
*excerpt_id,
excerpt.buffer_id,
excerpt.range.context.start,
))
}
pub fn set_excerpts_for_path(
&mut self,
path: Arc<Path>,
buffer: Entity<Buffer>,
ranges: Vec<Range<Point>>,
context_line_count: u32,
cx: &mut Context<Self>,
) {
let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
let (mut insert_after, excerpt_ids) =
if let Some(existing) = self.buffers_by_path.get(&path) {
(*existing.last().unwrap(), existing.clone())
} else {
(
self.buffers_by_path
.range(..path.clone())
.next_back()
.map(|(_, value)| *value.last().unwrap())
.unwrap_or(ExcerptId::min()),
Vec::default(),
)
};
let (new, _) = build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
let mut new_iter = new.into_iter().peekable();
let mut existing_iter = excerpt_ids.into_iter().peekable();
let mut new_excerpt_ids = Vec::new();
let mut to_remove = Vec::new();
let mut to_insert = Vec::new();
let snapshot = self.snapshot(cx);
let mut excerpts_cursor = snapshot.excerpts.cursor::<Option<&Locator>>(&());
excerpts_cursor.next(&());
loop {
let (new, existing) = match (new_iter.peek(), existing_iter.peek()) {
(Some(new), Some(existing)) => (new, existing),
(None, None) => break,
(None, Some(_)) => {
to_remove.push(existing_iter.next().unwrap());
continue;
}
(Some(_), None) => {
to_insert.push(new_iter.next().unwrap());
continue;
}
};
let locator = snapshot.excerpt_locator_for_id(*existing);
excerpts_cursor.seek_forward(&Some(locator), Bias::Left, &());
let existing_excerpt = excerpts_cursor.item().unwrap();
if existing_excerpt.buffer_id != buffer_snapshot.remote_id() {
to_remove.push(existing_iter.next().unwrap());
to_insert.push(new_iter.next().unwrap());
continue;
}
let existing_start = existing_excerpt
.range
.context
.start
.to_point(&buffer_snapshot);
let existing_end = existing_excerpt
.range
.context
.end
.to_point(&buffer_snapshot);
if existing_end < new.context.start {
to_remove.push(existing_iter.next().unwrap());
continue;
} else if existing_start > new.context.end {
to_insert.push(new_iter.next().unwrap());
continue;
}
// maybe merge overlapping excerpts?
// it's hard to distinguish between a manually expanded excerpt, and one that
// got smaller because of a missing diff.
//
if existing_start == new.context.start && existing_end == new.context.end {
new_excerpt_ids.append(&mut self.insert_excerpts_after(
insert_after,
buffer.clone(),
mem::take(&mut to_insert),
cx,
));
insert_after = existing_iter.next().unwrap();
new_excerpt_ids.push(insert_after);
new_iter.next();
} else {
to_remove.push(existing_iter.next().unwrap());
to_insert.push(new_iter.next().unwrap());
}
}
new_excerpt_ids.append(&mut self.insert_excerpts_after(
insert_after,
buffer,
to_insert,
cx,
));
self.remove_excerpts(to_remove, cx);
if new_excerpt_ids.is_empty() {
self.buffers_by_path.remove(&path);
} else {
self.buffers_by_path.insert(path, new_excerpt_ids);
}
}
pub fn paths(&self) -> impl Iterator<Item = Arc<Path>> + '_ {
self.buffers_by_path.keys().cloned()
}
pub fn remove_excerpts_for_path(&mut self, path: Arc<Path>, cx: &mut Context<Self>) {
if let Some(to_remove) = self.buffers_by_path.remove(&path) {
self.remove_excerpts(to_remove, cx)
}
}
pub fn push_multiple_excerpts_with_context_lines(
&self,
buffers_with_ranges: Vec<(Entity<Buffer>, Vec<Range<text::Anchor>>)>,
@ -1654,7 +1792,7 @@ impl MultiBuffer {
pub fn excerpts_for_buffer(
&self,
buffer: &Entity<Buffer>,
buffer_id: BufferId,
cx: &App,
) -> Vec<(ExcerptId, ExcerptRange<text::Anchor>)> {
let mut excerpts = Vec::new();
@ -1662,7 +1800,7 @@ impl MultiBuffer {
let buffers = self.buffers.borrow();
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>(&());
for locator in buffers
.get(&buffer.read(cx).remote_id())
.get(&buffer_id)
.map(|state| &state.excerpts)
.into_iter()
.flatten()
@ -1812,7 +1950,7 @@ impl MultiBuffer {
) -> Option<Anchor> {
let mut found = None;
let snapshot = buffer.read(cx).snapshot();
for (excerpt_id, range) in self.excerpts_for_buffer(buffer, cx) {
for (excerpt_id, range) in self.excerpts_for_buffer(snapshot.remote_id(), cx) {
let start = range.context.start.to_point(&snapshot);
let end = range.context.end.to_point(&snapshot);
if start <= point && point < end {
@ -4790,7 +4928,7 @@ impl MultiBufferSnapshot {
cursor.next_excerpt();
let mut visited_end = false;
iter::from_fn(move || {
iter::from_fn(move || loop {
if self.singleton {
return None;
}
@ -4800,7 +4938,8 @@ impl MultiBufferSnapshot {
let next_region_start = if let Some(region) = &next_region {
if !bounds.contains(&region.range.start.key) {
return None;
prev_region = next_region;
continue;
}
region.range.start.value.unwrap()
} else {
@ -4847,7 +4986,7 @@ impl MultiBufferSnapshot {
prev_region = next_region;
Some(ExcerptBoundary { row, prev, next })
return Some(ExcerptBoundary { row, prev, next });
})
}