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 });
})
}

View file

@ -6,7 +6,7 @@ use language::{Buffer, Rope};
use parking_lot::RwLock;
use rand::prelude::*;
use settings::SettingsStore;
use std::env;
use std::{env, path::PathBuf};
use util::test::sample_text;
#[ctor::ctor]
@ -315,7 +315,8 @@ fn test_excerpt_boundaries_and_clipping(cx: &mut App) {
);
let snapshot = multibuffer.update(cx, |multibuffer, cx| {
let (buffer_2_excerpt_id, _) = multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone();
let (buffer_2_excerpt_id, _) =
multibuffer.excerpts_for_buffer(buffer_2.read(cx).remote_id(), cx)[0].clone();
multibuffer.remove_excerpts([buffer_2_excerpt_id], cx);
multibuffer.snapshot(cx)
});
@ -1527,6 +1528,202 @@ fn test_repeatedly_expand_a_diff_hunk(cx: &mut TestAppContext) {
);
}
#[gpui::test]
fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) {
let buf1 = cx.new(|cx| {
Buffer::local(
indoc! {
"zero
one
two
three
four
five
six
seven
",
},
cx,
)
});
let path1: Arc<Path> = Arc::from(PathBuf::from("path1"));
let buf2 = cx.new(|cx| {
Buffer::local(
indoc! {
"000
111
222
333
444
555
666
777
888
999
"
},
cx,
)
});
let path2: Arc<Path> = Arc::from(PathBuf::from("path2"));
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
path1.clone(),
buf1.clone(),
vec![Point::row_range(0..1)],
2,
cx,
);
});
assert_excerpts_match(
&multibuffer,
cx,
indoc! {
"-----
zero
one
two
three
"
},
);
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(path1.clone(), buf1.clone(), vec![], 2, cx);
});
assert_excerpts_match(&multibuffer, cx, "");
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
path1.clone(),
buf1.clone(),
vec![Point::row_range(0..1), Point::row_range(7..8)],
2,
cx,
);
});
assert_excerpts_match(
&multibuffer,
cx,
indoc! {"-----
zero
one
two
three
-----
five
six
seven
"},
);
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
path1.clone(),
buf1.clone(),
vec![Point::row_range(0..1), Point::row_range(5..6)],
2,
cx,
);
});
assert_excerpts_match(
&multibuffer,
cx,
indoc! {"-----
zero
one
two
three
four
five
six
seven
"},
);
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
path2.clone(),
buf2.clone(),
vec![Point::row_range(2..3)],
2,
cx,
);
});
assert_excerpts_match(
&multibuffer,
cx,
indoc! {"-----
zero
one
two
three
four
five
six
seven
-----
000
111
222
333
444
555
"},
);
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(path1.clone(), buf1.clone(), vec![], 2, cx);
});
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
path1.clone(),
buf1.clone(),
vec![Point::row_range(3..4)],
2,
cx,
);
});
assert_excerpts_match(
&multibuffer,
cx,
indoc! {"-----
one
two
three
four
five
six
-----
000
111
222
333
444
555
"},
);
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
path1.clone(),
buf1.clone(),
vec![Point::row_range(3..4)],
2,
cx,
);
});
}
#[gpui::test]
fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) {
let base_text_1 = indoc!(
@ -2700,6 +2897,25 @@ fn format_diff(
.join("\n")
}
#[track_caller]
fn assert_excerpts_match(
multibuffer: &Entity<MultiBuffer>,
cx: &mut TestAppContext,
expected: &str,
) {
let mut output = String::new();
multibuffer.read_with(cx, |multibuffer, cx| {
for (_, buffer, range) in multibuffer.snapshot(cx).excerpts() {
output.push_str("-----\n");
output.extend(buffer.text_for_range(range.context));
if !output.ends_with('\n') {
output.push('\n');
}
}
});
assert_eq!(output, expected);
}
#[track_caller]
fn assert_new_snapshot(
multibuffer: &Entity<MultiBuffer>,