git: More rigorously test excerpt syncing for split diff, and fix a couple of bugs (#49907)
This PR adds a more rigorous test for the excerpt syncing logic in `SplittableEditor`, in preparation for refactoring that code, since we've had some bugs there. The new test covers - edits within the RHS multibuffer - edits to the individual main buffers, not necessarily within the excerpt bounds of the split diff - excerpt expansion - excerpt removal - excerpt recalculation based on diff hunk ranges Bugs fixed: - incorrect edit merging logic in `patches_for_range` - `merge_excerpts` leaving stale excerpt IDs in `excerpts_by_path` Before you mark this PR as ready for review, make sure that you have: - [x] Added a solid test coverage and/or screenshots from doing manual testing - [x] Done a self-review taking into account security and performance aspects - [ ] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Release Notes: - N/A
This commit is contained in:
1 parent
7ffd4c0a95
commit
00b9127309
4 files changed
+355
-153
No files matched your search
@@ -358,6 +358,19 @@ impl Companion {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn excerpt_mappings(
|
||||
&self,
|
||||
) -> (
|
||||
&HashMap<ExcerptId, ExcerptId>,
|
||||
&HashMap<ExcerptId, ExcerptId>,
|
||||
) {
|
||||
(
|
||||
&self.lhs_excerpt_to_rhs_excerpt,
|
||||
&self.rhs_excerpt_to_lhs_excerpt,
|
||||
)
|
||||
}
|
||||
|
||||
fn buffer_to_companion_buffer(&self, display_map_id: EntityId) -> &HashMap<BufferId, BufferId> {
|
||||
if self.is_rhs(display_map_id) {
|
||||
&self.rhs_buffer_to_lhs_buffer
|
||||
|
||||
+305
-141
@@ -337,7 +337,7 @@ fn patch_for_excerpt(
|
||||
let mut merged_edits: Vec<text::Edit<Point>> = Vec::new();
|
||||
for edit in edits {
|
||||
if let Some(last) = merged_edits.last_mut() {
|
||||
if edit.new.start <= last.new.end {
|
||||
if edit.new.start <= last.new.end || edit.old.start <= last.old.end {
|
||||
last.old.end = last.old.end.max(edit.old.end);
|
||||
last.new.end = last.new.end.max(edit.new.end);
|
||||
continue;
|
||||
@@ -1126,28 +1126,21 @@ impl SplittableEditor {
|
||||
#[cfg(test)]
|
||||
impl SplittableEditor {
|
||||
fn check_invariants(&self, quiesced: bool, cx: &mut App) {
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use text::Bias;
|
||||
|
||||
use crate::display_map::Block;
|
||||
use crate::display_map::DisplayRow;
|
||||
|
||||
self.debug_print(cx);
|
||||
self.check_excerpt_mapping_invariants(cx);
|
||||
|
||||
let lhs = self.lhs.as_ref().unwrap();
|
||||
let rhs_excerpts = self.rhs_multibuffer.read(cx).excerpt_ids();
|
||||
let lhs_excerpts = lhs.multibuffer.read(cx).excerpt_ids();
|
||||
assert_eq!(
|
||||
lhs_excerpts.len(),
|
||||
rhs_excerpts.len(),
|
||||
"mismatch in excerpt count"
|
||||
);
|
||||
|
||||
if quiesced {
|
||||
let rhs_snapshot = lhs
|
||||
let lhs_snapshot = lhs
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.display_snapshot(cx));
|
||||
let lhs_snapshot = self
|
||||
let rhs_snapshot = self
|
||||
.rhs_editor
|
||||
.update(cx, |editor, cx| editor.display_snapshot(cx));
|
||||
|
||||
@@ -1189,10 +1182,20 @@ impl SplittableEditor {
|
||||
|
||||
let (lhs_point, rhs_point) =
|
||||
if lhs_hunk.row_range.is_empty() || rhs_hunk.row_range.is_empty() {
|
||||
(
|
||||
Point::new(lhs_hunk.row_range.end.0, 0),
|
||||
Point::new(rhs_hunk.row_range.end.0, 0),
|
||||
)
|
||||
let lhs_end = Point::new(lhs_hunk.row_range.end.0, 0);
|
||||
let rhs_end = Point::new(rhs_hunk.row_range.end.0, 0);
|
||||
|
||||
let lhs_exceeds = lhs_snapshot
|
||||
.range_for_excerpt(lhs_hunk.excerpt_id)
|
||||
.map_or(false, |range| lhs_end >= range.end);
|
||||
let rhs_exceeds = rhs_snapshot
|
||||
.range_for_excerpt(rhs_hunk.excerpt_id)
|
||||
.map_or(false, |range| rhs_end >= range.end);
|
||||
if lhs_exceeds != rhs_exceeds {
|
||||
continue;
|
||||
}
|
||||
|
||||
(lhs_end, rhs_end)
|
||||
} else {
|
||||
(
|
||||
Point::new(lhs_hunk.row_range.start.0, 0),
|
||||
@@ -1207,44 +1210,6 @@ impl SplittableEditor {
|
||||
"mismatch in hunk position"
|
||||
);
|
||||
}
|
||||
|
||||
// Filtering out empty lines is a bit of a hack, to work around a case where
|
||||
// the base text has a trailing newline but the current text doesn't, or vice versa.
|
||||
// In this case, we get the additional newline on one side, but that line is not
|
||||
// marked as added/deleted by rowinfos.
|
||||
self.check_sides_match(cx, |snapshot| {
|
||||
snapshot
|
||||
.buffer_snapshot()
|
||||
.text()
|
||||
.split("\n")
|
||||
.zip(snapshot.buffer_snapshot().row_infos(MultiBufferRow(0)))
|
||||
.filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
|
||||
.map(|(line, _)| line.to_owned())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn check_sides_match<T: std::fmt::Debug + PartialEq>(
|
||||
&self,
|
||||
cx: &mut App,
|
||||
mut extract: impl FnMut(&crate::DisplaySnapshot) -> T,
|
||||
) {
|
||||
let lhs = self.lhs.as_ref().expect("requires split");
|
||||
let rhs_snapshot = self.rhs_editor.update(cx, |editor, cx| {
|
||||
editor.display_map.update(cx, |map, cx| map.snapshot(cx))
|
||||
});
|
||||
let lhs_snapshot = lhs.editor.update(cx, |editor, cx| {
|
||||
editor.display_map.update(cx, |map, cx| map.snapshot(cx))
|
||||
});
|
||||
|
||||
let rhs_t = extract(&rhs_snapshot);
|
||||
let lhs_t = extract(&lhs_snapshot);
|
||||
|
||||
if rhs_t != lhs_t {
|
||||
self.debug_print(cx);
|
||||
pretty_assertions::assert_eq!(rhs_t, lhs_t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1519,87 +1484,109 @@ impl SplittableEditor {
|
||||
eprintln!();
|
||||
}
|
||||
|
||||
fn randomly_edit_excerpts(
|
||||
&mut self,
|
||||
rng: &mut impl rand::Rng,
|
||||
mutation_count: usize,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
use collections::HashSet;
|
||||
use rand::prelude::*;
|
||||
use std::env;
|
||||
use util::RandomCharIter;
|
||||
fn check_excerpt_mapping_invariants(&self, cx: &gpui::App) {
|
||||
use multi_buffer::{ExcerptId, PathKey};
|
||||
|
||||
let max_buffers = env::var("MAX_BUFFERS")
|
||||
.map(|i| i.parse().expect("invalid `MAX_BUFFERS` variable"))
|
||||
.unwrap_or(4);
|
||||
let lhs = self.lhs.as_ref().expect("should have lhs editor");
|
||||
|
||||
for _ in 0..mutation_count {
|
||||
let paths = self
|
||||
let rhs_excerpt_ids = self.rhs_multibuffer.read(cx).excerpt_ids();
|
||||
let lhs_excerpt_ids = lhs.multibuffer.read(cx).excerpt_ids();
|
||||
assert_eq!(
|
||||
rhs_excerpt_ids.len(),
|
||||
lhs_excerpt_ids.len(),
|
||||
"excerpt count mismatch: rhs has {}, lhs has {}",
|
||||
rhs_excerpt_ids.len(),
|
||||
lhs_excerpt_ids.len(),
|
||||
);
|
||||
|
||||
let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
|
||||
let companion = rhs_display_map
|
||||
.read(cx)
|
||||
.companion()
|
||||
.cloned()
|
||||
.expect("should have companion");
|
||||
let (lhs_to_rhs, rhs_to_lhs) = {
|
||||
let c = companion.read(cx);
|
||||
let (l, r) = c.excerpt_mappings();
|
||||
(l.clone(), r.clone())
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
lhs_to_rhs.len(),
|
||||
rhs_to_lhs.len(),
|
||||
"mapping size mismatch: lhs_to_rhs has {}, rhs_to_lhs has {}",
|
||||
lhs_to_rhs.len(),
|
||||
rhs_to_lhs.len(),
|
||||
);
|
||||
|
||||
for (&lhs_id, &rhs_id) in &lhs_to_rhs {
|
||||
let reverse = rhs_to_lhs.get(&rhs_id);
|
||||
assert_eq!(
|
||||
reverse,
|
||||
Some(&lhs_id),
|
||||
"lhs_to_rhs maps {lhs_id:?} -> {rhs_id:?}, but rhs_to_lhs maps {rhs_id:?} -> {reverse:?}",
|
||||
);
|
||||
}
|
||||
for (&rhs_id, &lhs_id) in &rhs_to_lhs {
|
||||
let reverse = lhs_to_rhs.get(&lhs_id);
|
||||
assert_eq!(
|
||||
reverse,
|
||||
Some(&rhs_id),
|
||||
"rhs_to_lhs maps {rhs_id:?} -> {lhs_id:?}, but lhs_to_rhs maps {lhs_id:?} -> {reverse:?}",
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
lhs_to_rhs.len(),
|
||||
rhs_excerpt_ids.len(),
|
||||
"mapping covers {} excerpts but rhs has {}",
|
||||
lhs_to_rhs.len(),
|
||||
rhs_excerpt_ids.len(),
|
||||
);
|
||||
|
||||
let rhs_mapped_order: Vec<ExcerptId> = rhs_excerpt_ids
|
||||
.iter()
|
||||
.map(|rhs_id| {
|
||||
*rhs_to_lhs.get(rhs_id).unwrap_or_else(|| {
|
||||
panic!("rhs excerpt {rhs_id:?} has no mapping in rhs_to_lhs")
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
rhs_mapped_order, lhs_excerpt_ids,
|
||||
"excerpt ordering mismatch: mapping rhs order through rhs_to_lhs doesn't match lhs order",
|
||||
);
|
||||
|
||||
let rhs_paths: Vec<PathKey> = self.rhs_multibuffer.read(cx).paths().cloned().collect();
|
||||
let lhs_paths: Vec<PathKey> = lhs.multibuffer.read(cx).paths().cloned().collect();
|
||||
assert_eq!(
|
||||
rhs_paths, lhs_paths,
|
||||
"path set mismatch between rhs and lhs"
|
||||
);
|
||||
|
||||
for path in &rhs_paths {
|
||||
let rhs_path_excerpts: Vec<ExcerptId> = self
|
||||
.rhs_multibuffer
|
||||
.read(cx)
|
||||
.paths()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let excerpt_ids = self.rhs_multibuffer.read(cx).excerpt_ids();
|
||||
|
||||
if rng.random_bool(0.2) && !excerpt_ids.is_empty() {
|
||||
let mut excerpts = HashSet::default();
|
||||
for _ in 0..rng.random_range(0..excerpt_ids.len()) {
|
||||
excerpts.extend(excerpt_ids.choose(rng).copied());
|
||||
}
|
||||
|
||||
let line_count = rng.random_range(1..5);
|
||||
|
||||
log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
|
||||
|
||||
self.expand_excerpts(
|
||||
excerpts.iter().cloned(),
|
||||
line_count,
|
||||
ExpandExcerptDirection::UpAndDown,
|
||||
cx,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if excerpt_ids.is_empty() || (rng.random_bool(0.8) && paths.len() < max_buffers) {
|
||||
let len = rng.random_range(100..500);
|
||||
let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||||
log::info!(
|
||||
"Creating new buffer {} with text: {:?}",
|
||||
buffer.read(cx).remote_id(),
|
||||
buffer.read(cx).text()
|
||||
);
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
|
||||
// Create some initial diff hunks.
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.randomly_edit(rng, 1, cx);
|
||||
});
|
||||
let buffer_snapshot = buffer.read(cx).text_snapshot();
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
||||
});
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let ranges = diff.update(cx, |diff, cx| {
|
||||
diff.snapshot(cx)
|
||||
.hunks(&buffer_snapshot)
|
||||
.map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
|
||||
} else {
|
||||
log::info!("removing excerpts");
|
||||
let remove_count = rng.random_range(1..=paths.len());
|
||||
let paths_to_remove = paths
|
||||
.choose_multiple(rng, remove_count)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
for path in paths_to_remove {
|
||||
self.remove_excerpts_for_path(path.clone(), cx);
|
||||
}
|
||||
}
|
||||
.excerpts_for_path(path)
|
||||
.collect();
|
||||
let lhs_path_excerpts: Vec<ExcerptId> =
|
||||
lhs.multibuffer.read(cx).excerpts_for_path(path).collect();
|
||||
assert_eq!(
|
||||
rhs_path_excerpts.len(),
|
||||
lhs_path_excerpts.len(),
|
||||
"excerpt count mismatch for path {path:?}: rhs has {}, lhs has {}",
|
||||
rhs_path_excerpts.len(),
|
||||
lhs_path_excerpts.len(),
|
||||
);
|
||||
let rhs_path_mapped: Vec<ExcerptId> = rhs_path_excerpts
|
||||
.iter()
|
||||
.map(|rhs_id| *rhs_to_lhs.get(rhs_id).unwrap())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
rhs_path_mapped, lhs_path_excerpts,
|
||||
"per-path excerpt ordering mismatch for {path:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2275,7 +2262,9 @@ mod tests {
|
||||
|
||||
#[gpui::test(iterations = 25)]
|
||||
async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
|
||||
use multi_buffer::ExpandExcerptDirection;
|
||||
use rand::prelude::*;
|
||||
use util::RandomCharIter;
|
||||
|
||||
let (editor, cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
||||
let operations = std::env::var("OPERATIONS")
|
||||
@@ -2288,9 +2277,32 @@ mod tests {
|
||||
});
|
||||
|
||||
if buffers.is_empty() {
|
||||
log::info!("adding excerpts to empty multibuffer");
|
||||
log::info!("creating initial buffer");
|
||||
let len = rng.random_range(200..1000);
|
||||
let base_text: String = RandomCharIter::new(&mut *rng).take(len).collect();
|
||||
let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx));
|
||||
let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
|
||||
let diff =
|
||||
cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer_snapshot, cx));
|
||||
let edit_count = rng.random_range(3..8);
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.randomly_edit(rng, edit_count, cx);
|
||||
});
|
||||
let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
||||
});
|
||||
let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
|
||||
let ranges = diff_snapshot
|
||||
.hunks(&buffer_snapshot)
|
||||
.map(|hunk| hunk.range)
|
||||
.collect::<Vec<_>>();
|
||||
let context_lines = rng.random_range(0..2);
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
editor.set_excerpts_for_path(path, buffer, ranges, context_lines, diff, cx);
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.randomly_edit_excerpts(rng, 2, cx);
|
||||
editor.check_invariants(true, cx);
|
||||
});
|
||||
continue;
|
||||
@@ -2299,26 +2311,129 @@ mod tests {
|
||||
let mut quiesced = false;
|
||||
|
||||
match rng.random_range(0..100) {
|
||||
0..=44 => {
|
||||
0..=14 if buffers.len() < 6 => {
|
||||
log::info!("creating new buffer and setting excerpts");
|
||||
let len = rng.random_range(200..1000);
|
||||
let base_text: String = RandomCharIter::new(&mut *rng).take(len).collect();
|
||||
let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx));
|
||||
let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
|
||||
let diff = cx
|
||||
.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer_snapshot, cx));
|
||||
let edit_count = rng.random_range(3..8);
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.randomly_edit(rng, edit_count, cx);
|
||||
});
|
||||
let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
||||
});
|
||||
let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
|
||||
let ranges = diff_snapshot
|
||||
.hunks(&buffer_snapshot)
|
||||
.map(|hunk| hunk.range)
|
||||
.collect::<Vec<_>>();
|
||||
let context_lines = rng.random_range(0..2);
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
editor.set_excerpts_for_path(path, buffer, ranges, context_lines, diff, cx);
|
||||
});
|
||||
}
|
||||
15..=29 => {
|
||||
log::info!("randomly editing multibuffer");
|
||||
let edit_count = rng.random_range(1..5);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.rhs_multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.randomly_edit(rng, 5, cx);
|
||||
})
|
||||
})
|
||||
multibuffer.randomly_edit(rng, edit_count, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
45..=64 => {
|
||||
30..=44 => {
|
||||
log::info!("randomly editing individual buffer");
|
||||
let buffer = buffers.iter().choose(rng).unwrap();
|
||||
let edit_count = rng.random_range(1..3);
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.randomly_edit(rng, edit_count, cx);
|
||||
});
|
||||
}
|
||||
45..=54 => {
|
||||
log::info!("recalculating diff and resetting excerpts for single buffer");
|
||||
let buffer = buffers.iter().choose(rng).unwrap();
|
||||
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
||||
let diff = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.rhs_multibuffer
|
||||
.read(cx)
|
||||
.diff_for(buffer.read(cx).remote_id())
|
||||
.unwrap()
|
||||
});
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
|
||||
let ranges = diff_snapshot
|
||||
.hunks(&buffer_snapshot)
|
||||
.map(|hunk| hunk.range)
|
||||
.collect::<Vec<_>>();
|
||||
let context_lines = rng.random_range(0..2);
|
||||
let buffer = buffer.clone();
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
editor.set_excerpts_for_path(path, buffer, ranges, context_lines, diff, cx);
|
||||
});
|
||||
}
|
||||
55..=64 => {
|
||||
log::info!("randomly undoing/redoing in single buffer");
|
||||
let buffer = buffers.iter().choose(rng).unwrap();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.randomly_undo_redo(rng, cx);
|
||||
});
|
||||
}
|
||||
65..=79 => {
|
||||
log::info!("mutating excerpts");
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.randomly_edit_excerpts(rng, 2, cx);
|
||||
65..=74 => {
|
||||
log::info!("removing excerpts for a random path");
|
||||
let paths = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.rhs_multibuffer
|
||||
.read(cx)
|
||||
.paths()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
if let Some(path) = paths.choose(rng) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.remove_excerpts_for_path(path.clone(), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
75..=79 => {
|
||||
log::info!("unsplit and resplit");
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.unsplit(window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.split(window, cx);
|
||||
});
|
||||
}
|
||||
80..=89 => {
|
||||
let excerpt_ids = editor.update(cx, |editor, cx| {
|
||||
editor.rhs_multibuffer.read(cx).excerpt_ids()
|
||||
});
|
||||
if !excerpt_ids.is_empty() {
|
||||
let count = rng.random_range(1..=excerpt_ids.len().min(3));
|
||||
let chosen: Vec<_> =
|
||||
excerpt_ids.choose_multiple(rng, count).copied().collect();
|
||||
let line_count = rng.random_range(1..5);
|
||||
log::info!("expanding {count} excerpts by {line_count} lines");
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.expand_excerpts(
|
||||
chosen.into_iter(),
|
||||
line_count,
|
||||
ExpandExcerptDirection::UpAndDown,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::info!("quiescing");
|
||||
@@ -2356,6 +2471,55 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_expand_excerpt_with_hunk_before_excerpt_start(cx: &mut gpui::TestAppContext) {
|
||||
use rope::Point;
|
||||
|
||||
let (editor, cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
|
||||
|
||||
let base_text = "aaaaaaa rest_of_line\nsecond_line\nthird_line\nfourth_line";
|
||||
let current_text = "aaaaaaa rest_of_line\nsecond_line\nMODIFIED\nfourth_line";
|
||||
let (buffer, diff) = buffer_with_diff(base_text, current_text, cx);
|
||||
|
||||
let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
|
||||
let ranges = diff_snapshot
|
||||
.hunks(&buffer_snapshot)
|
||||
.map(|hunk| hunk.range)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
editor.set_excerpts_for_path(path, buffer.clone(), ranges, 0, diff.clone(), cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[(Point::new(0, 7)..Point::new(1, 7), "\nnew_line\n")],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let excerpt_ids = editor.update(cx, |editor, cx| {
|
||||
editor.rhs_multibuffer.read(cx).excerpt_ids()
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.expand_excerpts(
|
||||
excerpt_ids.iter().copied(),
|
||||
2,
|
||||
multi_buffer::ExpandExcerptDirection::UpAndDown,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_basic_alignment(cx: &mut gpui::TestAppContext) {
|
||||
use rope::Point;
|
||||
|
||||
@@ -595,14 +595,12 @@ impl DiffState {
|
||||
base_text_changed_range,
|
||||
extended_range: _,
|
||||
}) => {
|
||||
if let Some(base_text_changed_range) = base_text_changed_range.clone() {
|
||||
this.inverted_buffer_diff_changed(
|
||||
diff,
|
||||
main_buffer,
|
||||
base_text_changed_range,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
this.inverted_buffer_diff_changed(
|
||||
diff,
|
||||
main_buffer,
|
||||
base_text_changed_range.clone(),
|
||||
cx,
|
||||
);
|
||||
cx.emit(Event::BufferDiffChanged);
|
||||
}
|
||||
BufferDiffEvent::LanguageChanged => {
|
||||
@@ -2202,7 +2200,19 @@ impl MultiBuffer {
|
||||
drop(snapshot);
|
||||
|
||||
self.resize_excerpt(excerpt_ids[0], union_range, cx);
|
||||
self.remove_excerpts(excerpt_ids[1..].iter().copied(), cx);
|
||||
let removed = &excerpt_ids[1..];
|
||||
for &excerpt_id in removed {
|
||||
if let Some(path) = self.paths_by_excerpt.get(&excerpt_id) {
|
||||
if let Some(excerpt_list) = self.excerpts_by_path.get_mut(path) {
|
||||
excerpt_list.retain(|id| *id != excerpt_id);
|
||||
if excerpt_list.is_empty() {
|
||||
let path = path.clone();
|
||||
self.excerpts_by_path.remove(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.remove_excerpts(removed.iter().copied(), cx);
|
||||
|
||||
excerpt_ids[0]
|
||||
}
|
||||
@@ -2489,7 +2499,7 @@ impl MultiBuffer {
|
||||
&mut self,
|
||||
diff: Entity<BufferDiff>,
|
||||
main_buffer: Entity<language::Buffer>,
|
||||
diff_change_range: Range<usize>,
|
||||
diff_change_range: Option<Range<usize>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.sync_mut(cx);
|
||||
@@ -2510,6 +2520,10 @@ impl MultiBuffer {
|
||||
.diffs
|
||||
.insert_or_replace(base_text_buffer_id, new_diff);
|
||||
|
||||
let Some(diff_change_range) = diff_change_range else {
|
||||
return;
|
||||
};
|
||||
|
||||
let excerpt_edits = snapshot.excerpt_edits_for_diff_change(buffer_state, diff_change_range);
|
||||
let edits = Self::sync_diff_transforms(
|
||||
&mut snapshot,
|
||||
@@ -2705,7 +2719,12 @@ impl MultiBuffer {
|
||||
let base_text_buffer_id = snapshot.remote_id();
|
||||
let diff_change_range = 0..snapshot.len();
|
||||
self.snapshot.get_mut().has_inverted_diff = true;
|
||||
self.inverted_buffer_diff_changed(diff.clone(), main_buffer.clone(), diff_change_range, cx);
|
||||
self.inverted_buffer_diff_changed(
|
||||
diff.clone(),
|
||||
main_buffer.clone(),
|
||||
Some(diff_change_range),
|
||||
cx,
|
||||
);
|
||||
self.diffs.insert(
|
||||
base_text_buffer_id,
|
||||
DiffState::new_inverted(diff, main_buffer, cx),
|
||||
|
||||
@@ -188,7 +188,13 @@ impl MultiBuffer {
|
||||
direction: ExpandExcerptDirection,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let grouped = ids
|
||||
let mut sorted_ids: Vec<ExcerptId> = ids.into_iter().collect();
|
||||
sorted_ids.sort_by(|a, b| {
|
||||
let path_a = self.paths_by_excerpt.get(a);
|
||||
let path_b = self.paths_by_excerpt.get(b);
|
||||
path_a.cmp(&path_b)
|
||||
});
|
||||
let grouped = sorted_ids
|
||||
.into_iter()
|
||||
.chunk_by(|id| self.paths_by_excerpt.get(id).cloned())
|
||||
.into_iter()
|
||||
|
||||
Reference in new issue
Block a user