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:
parent
27a413a5e3
commit
45708d2680
21 changed files with 1023 additions and 125 deletions
|
@ -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(®ion.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 });
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue