This PR changes how base texts are managed by the `buffer_diff` crate, to enable keeping two diff entities alive that share the same base text buffer entity. Previously, each diff owned its own base text buffer and edited it when calling `BufferDiff::set_snapshot`, so the only way to reuse the same base text between two diffs was to have two independent buffers for it, which is pretty inefficient. After this PR, each diff still has a base text buffer, but `set_snapshot` doesn't edit it. Instead, that responsibility moves into the caller. For updating the base text buffer, this PR also introduces a new pair of APIs, `Buffer::snapshot_with_edits` and `Buffer::fast_forward`, which allow us to move the parsing of the new base text into the background and then install the new syntax tree synchronously on the foreground. The git store uses the low-level APIs `set_snapshot` and `fast_forward` directly, and manages the head text and index text buffers itself (garbage-collecting them when they're no longer needed); this enables adding an `open_staged_diff` API which returns a diff between the managed index buffer and the managed head buffer (the latter is also used for the uncommitted diff's base text). Other downstreams don't need to reuse a base text buffer, and those have been migrated to use the high-level `set_base_text` API, which now calls `set_snapshot` and `fast_forward` internally, with a guard to prevent concurrent updates. Another change worthy of note is that we now always diff the old base text with the new base text to create `snapshot_with_edits`. There are also some incidental bug fixes: - Fixed an issue where a dangling weak unstaged diff could stick around in the git store forever - Restored the `IndexMatchesHead` optimization that had become inoperative in the remote case - Fixed a crash in the multibuffer due to the handling of `BufferDiffEvent::LanguageChanged`, which could cause the multibuffer to have transforms that were inconsistent with the diff base text. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes ZED-81P Release Notes: - Fixed a rare crash that could occur while using the uncommitted diff. --------- Co-authored-by: Ben Kunkle <ben@zed.dev> Co-authored-by: Anthony Eid <hello@anthonyeid.me>
6277 lines
194 KiB
Rust
6277 lines
194 KiB
Rust
use super::*;
|
||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||
use gpui::{App, Entity, TestAppContext};
|
||
use indoc::indoc;
|
||
use language::{Buffer, Rope};
|
||
use parking_lot::RwLock;
|
||
use rand::prelude::*;
|
||
use settings::SettingsStore;
|
||
use std::env;
|
||
use std::sync::Arc;
|
||
use std::time::{Duration, Instant};
|
||
use util::RandomCharIter;
|
||
use util::rel_path::rel_path;
|
||
use util::test::sample_text;
|
||
|
||
#[ctor::ctor(unsafe)]
|
||
fn init_logger() {
|
||
zlog::init_test();
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_empty_singleton(cx: &mut App) {
|
||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||
let buffer_id = buffer.read(cx).remote_id();
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
assert_eq!(snapshot.text(), "");
|
||
assert_eq!(
|
||
snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>(),
|
||
[RowInfo {
|
||
buffer_id: Some(buffer_id),
|
||
buffer_row: Some(0),
|
||
multibuffer_row: Some(MultiBufferRow(0)),
|
||
diff_status: None,
|
||
expand_info: None,
|
||
wrapped_buffer_row: None,
|
||
}]
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_singleton(cx: &mut App) {
|
||
let buffer = cx.new(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
assert_eq!(snapshot.text(), buffer.read(cx).text());
|
||
|
||
assert_eq!(
|
||
snapshot
|
||
.row_infos(MultiBufferRow(0))
|
||
.map(|info| info.buffer_row)
|
||
.collect::<Vec<_>>(),
|
||
(0..buffer.read(cx).row_count())
|
||
.map(Some)
|
||
.collect::<Vec<_>>()
|
||
);
|
||
assert_consistent_line_numbers(&snapshot);
|
||
|
||
buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], None, cx));
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
|
||
assert_eq!(snapshot.text(), buffer.read(cx).text());
|
||
assert_eq!(
|
||
snapshot
|
||
.row_infos(MultiBufferRow(0))
|
||
.map(|info| info.buffer_row)
|
||
.collect::<Vec<_>>(),
|
||
(0..buffer.read(cx).row_count())
|
||
.map(Some)
|
||
.collect::<Vec<_>>()
|
||
);
|
||
assert_consistent_line_numbers(&snapshot);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_buffer_point_to_anchor_at_end_of_singleton_buffer(cx: &mut App) {
|
||
let buffer = cx.new(|cx| Buffer::local("abc", cx));
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||
|
||
let anchor = multibuffer
|
||
.read(cx)
|
||
.buffer_point_to_anchor(&buffer, Point::new(0, 3), cx)
|
||
.unwrap();
|
||
let (anchor, _) = multibuffer
|
||
.read(cx)
|
||
.snapshot(cx)
|
||
.anchor_to_buffer_anchor(anchor)
|
||
.unwrap();
|
||
|
||
assert_eq!(
|
||
anchor,
|
||
buffer.read(cx).snapshot().anchor_after(Point::new(0, 3)),
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_remote(cx: &mut App) {
|
||
let host_buffer = cx.new(|cx| Buffer::local("a", cx));
|
||
let guest_buffer = cx.new(|cx| {
|
||
let state = host_buffer.read(cx).to_proto(cx);
|
||
let ops = cx
|
||
.foreground_executor()
|
||
.block_on(host_buffer.read(cx).serialize_ops(None, cx));
|
||
let mut buffer =
|
||
Buffer::from_proto(ReplicaId::REMOTE_SERVER, Capability::ReadWrite, state, None)
|
||
.unwrap();
|
||
buffer.apply_ops(
|
||
ops.into_iter()
|
||
.map(|op| language::proto::deserialize_operation(op).unwrap()),
|
||
cx,
|
||
);
|
||
buffer
|
||
});
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(guest_buffer.clone(), cx));
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
assert_eq!(snapshot.text(), "a");
|
||
|
||
guest_buffer.update(cx, |buffer, cx| buffer.edit([(1..1, "b")], None, cx));
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
assert_eq!(snapshot.text(), "ab");
|
||
|
||
guest_buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "c")], None, cx));
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
assert_eq!(snapshot.text(), "abc");
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_excerpt_boundaries_and_clipping(cx: &mut App) {
|
||
let buffer_1 = cx.new(|cx| Buffer::local(sample_text(7, 6, 'a'), cx));
|
||
let buffer_2 = cx.new(|cx| Buffer::local(sample_text(7, 6, 'g'), cx));
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
|
||
let events = Arc::new(RwLock::new(Vec::<Event>::new()));
|
||
multibuffer.update(cx, |_, cx| {
|
||
let events = events.clone();
|
||
cx.subscribe(&multibuffer, move |_, _, event, _| {
|
||
if let Event::Edited { .. } = event {
|
||
events.write().push(event.clone())
|
||
}
|
||
})
|
||
.detach();
|
||
});
|
||
|
||
let subscription = multibuffer.update(cx, |multibuffer, cx| {
|
||
let subscription = multibuffer.subscribe();
|
||
multibuffer.set_excerpt_ranges_for_path(
|
||
PathKey::sorted(0),
|
||
buffer_1.clone(),
|
||
&buffer_1.read(cx).snapshot(),
|
||
vec![ExcerptRange::new(Point::new(1, 2)..Point::new(2, 5))],
|
||
cx,
|
||
);
|
||
assert_eq!(
|
||
subscription.consume().into_inner(),
|
||
[Edit {
|
||
old: MultiBufferOffset(0)..MultiBufferOffset(0),
|
||
new: MultiBufferOffset(0)..MultiBufferOffset(10)
|
||
}]
|
||
);
|
||
|
||
multibuffer.set_excerpt_ranges_for_path(
|
||
PathKey::sorted(0),
|
||
buffer_1.clone(),
|
||
&buffer_1.read(cx).snapshot(),
|
||
vec![
|
||
ExcerptRange::new(Point::new(1, 2)..Point::new(2, 5)),
|
||
ExcerptRange::new(Point::new(5, 3)..Point::new(6, 4)),
|
||
],
|
||
cx,
|
||
);
|
||
multibuffer.set_excerpt_ranges_for_path(
|
||
PathKey::sorted(1),
|
||
buffer_2.clone(),
|
||
&buffer_2.read(cx).snapshot(),
|
||
vec![ExcerptRange::new(Point::new(3, 1)..Point::new(3, 3))],
|
||
cx,
|
||
);
|
||
assert_eq!(
|
||
subscription.consume().into_inner(),
|
||
[Edit {
|
||
old: MultiBufferOffset(10)..MultiBufferOffset(10),
|
||
new: MultiBufferOffset(10)..MultiBufferOffset(22)
|
||
}]
|
||
);
|
||
|
||
subscription
|
||
});
|
||
|
||
// Adding excerpts emits an edited event.
|
||
assert_eq!(
|
||
events.read().as_slice(),
|
||
&[
|
||
Event::Edited {
|
||
edited_buffer: None,
|
||
source: language::BufferEditSource::User,
|
||
},
|
||
Event::Edited {
|
||
edited_buffer: None,
|
||
source: language::BufferEditSource::User,
|
||
},
|
||
Event::Edited {
|
||
edited_buffer: None,
|
||
source: language::BufferEditSource::User,
|
||
}
|
||
]
|
||
);
|
||
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
assert_eq!(
|
||
snapshot.text(),
|
||
indoc!(
|
||
"
|
||
bbbb
|
||
ccccc
|
||
fff
|
||
gggg
|
||
jj"
|
||
),
|
||
);
|
||
assert_eq!(
|
||
snapshot
|
||
.row_infos(MultiBufferRow(0))
|
||
.map(|info| info.buffer_row)
|
||
.collect::<Vec<_>>(),
|
||
[Some(1), Some(2), Some(5), Some(6), Some(3)]
|
||
);
|
||
assert_eq!(
|
||
snapshot
|
||
.row_infos(MultiBufferRow(2))
|
||
.map(|info| info.buffer_row)
|
||
.collect::<Vec<_>>(),
|
||
[Some(5), Some(6), Some(3)]
|
||
);
|
||
assert_eq!(
|
||
snapshot
|
||
.row_infos(MultiBufferRow(4))
|
||
.map(|info| info.buffer_row)
|
||
.collect::<Vec<_>>(),
|
||
[Some(3)]
|
||
);
|
||
assert!(
|
||
snapshot
|
||
.row_infos(MultiBufferRow(5))
|
||
.map(|info| info.buffer_row)
|
||
.collect::<Vec<_>>()
|
||
.is_empty()
|
||
);
|
||
|
||
assert_eq!(
|
||
boundaries_in_range(Point::new(0, 0)..Point::new(4, 2), &snapshot),
|
||
&[
|
||
(MultiBufferRow(0), "bbbb\nccccc".to_string(), true),
|
||
(MultiBufferRow(2), "fff\ngggg".to_string(), false),
|
||
(MultiBufferRow(4), "jj".to_string(), true),
|
||
]
|
||
);
|
||
assert_eq!(
|
||
boundaries_in_range(Point::new(0, 0)..Point::new(2, 0), &snapshot),
|
||
&[(MultiBufferRow(0), "bbbb\nccccc".to_string(), true)]
|
||
);
|
||
assert_eq!(
|
||
boundaries_in_range(Point::new(1, 0)..Point::new(1, 5), &snapshot),
|
||
&[]
|
||
);
|
||
assert_eq!(
|
||
boundaries_in_range(Point::new(1, 0)..Point::new(2, 0), &snapshot),
|
||
&[]
|
||
);
|
||
assert_eq!(
|
||
boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot),
|
||
&[(MultiBufferRow(2), "fff\ngggg".to_string(), false)]
|
||
);
|
||
assert_eq!(
|
||
boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot),
|
||
&[(MultiBufferRow(2), "fff\ngggg".to_string(), false)]
|
||
);
|
||
assert_eq!(
|
||
boundaries_in_range(Point::new(2, 0)..Point::new(3, 0), &snapshot),
|
||
&[(MultiBufferRow(2), "fff\ngggg".to_string(), false)]
|
||
);
|
||
assert_eq!(
|
||
boundaries_in_range(Point::new(4, 0)..Point::new(4, 2), &snapshot),
|
||
&[(MultiBufferRow(4), "jj".to_string(), true)]
|
||
);
|
||
assert_eq!(
|
||
boundaries_in_range(Point::new(4, 2)..Point::new(4, 2), &snapshot),
|
||
&[]
|
||
);
|
||
|
||
buffer_1.update(cx, |buffer, cx| {
|
||
let text = "\n";
|
||
buffer.edit(
|
||
[
|
||
(Point::new(0, 0)..Point::new(0, 0), text),
|
||
(Point::new(2, 1)..Point::new(2, 3), text),
|
||
],
|
||
None,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
assert_eq!(
|
||
snapshot.text(),
|
||
concat!(
|
||
"bbbb\n", // Preserve newlines
|
||
"c\n", //
|
||
"cc\n", //
|
||
"fff\n", //
|
||
"gggg\n", //
|
||
"jj" //
|
||
)
|
||
);
|
||
|
||
assert_eq!(
|
||
subscription.consume().into_inner(),
|
||
[Edit {
|
||
old: MultiBufferOffset(6)..MultiBufferOffset(8),
|
||
new: MultiBufferOffset(6)..MultiBufferOffset(7)
|
||
}]
|
||
);
|
||
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
assert_eq!(
|
||
snapshot.clip_point(Point::new(0, 5), Bias::Left),
|
||
Point::new(0, 4)
|
||
);
|
||
assert_eq!(
|
||
snapshot.clip_point(Point::new(0, 5), Bias::Right),
|
||
Point::new(0, 4)
|
||
);
|
||
assert_eq!(
|
||
snapshot.clip_point(Point::new(5, 1), Bias::Right),
|
||
Point::new(5, 1)
|
||
);
|
||
assert_eq!(
|
||
snapshot.clip_point(Point::new(5, 2), Bias::Right),
|
||
Point::new(5, 2)
|
||
);
|
||
assert_eq!(
|
||
snapshot.clip_point(Point::new(5, 3), Bias::Right),
|
||
Point::new(5, 2)
|
||
);
|
||
|
||
let snapshot = multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.remove_excerpts(PathKey::sorted(1), cx);
|
||
multibuffer.snapshot(cx)
|
||
});
|
||
|
||
assert_eq!(
|
||
snapshot.text(),
|
||
concat!(
|
||
"bbbb\n", // Preserve newlines
|
||
"c\n", //
|
||
"cc\n", //
|
||
"fff\n", //
|
||
"gggg", //
|
||
)
|
||
);
|
||
|
||
fn boundaries_in_range(
|
||
range: Range<Point>,
|
||
snapshot: &MultiBufferSnapshot,
|
||
) -> Vec<(MultiBufferRow, String, bool)> {
|
||
snapshot
|
||
.excerpt_boundaries_in_range(range)
|
||
.map(|boundary| {
|
||
let starts_new_buffer = boundary.starts_new_buffer();
|
||
(
|
||
boundary.row,
|
||
boundary
|
||
.next
|
||
.buffer(snapshot)
|
||
.text_for_range(boundary.next.range.context)
|
||
.collect::<String>(),
|
||
starts_new_buffer,
|
||
)
|
||
})
|
||
.collect::<Vec<_>>()
|
||
}
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_diff_boundary_anchors(cx: &mut TestAppContext) {
|
||
let base_text = "one\ntwo\nthree\n";
|
||
let text = "one\nthree\n";
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||
let diff = cx
|
||
.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||
multibuffer.update(cx, |multibuffer, cx| multibuffer.add_diff(diff, cx));
|
||
|
||
let (before, after) = multibuffer.update(cx, |multibuffer, cx| {
|
||
let before = multibuffer.snapshot(cx).anchor_before(Point::new(1, 0));
|
||
let after = multibuffer.snapshot(cx).anchor_after(Point::new(1, 0));
|
||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||
(before, after)
|
||
});
|
||
cx.run_until_parked();
|
||
|
||
let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||
let actual_text = snapshot.text();
|
||
let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
|
||
let actual_diff = format_diff(&actual_text, &actual_row_infos, &Default::default(), None);
|
||
pretty_assertions::assert_eq!(
|
||
actual_diff,
|
||
indoc! {
|
||
" one
|
||
- two
|
||
three
|
||
"
|
||
},
|
||
);
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
assert_eq!(before.to_point(&snapshot), Point::new(1, 0));
|
||
assert_eq!(after.to_point(&snapshot), Point::new(2, 0));
|
||
assert_eq!(
|
||
vec![Point::new(1, 0), Point::new(2, 0),],
|
||
snapshot.summaries_for_anchors::<Point, _>(&[before, after]),
|
||
)
|
||
})
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
|
||
let base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n";
|
||
let text = "one\nfour\nseven\n";
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||
let diff = cx
|
||
.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||
(multibuffer.snapshot(cx), multibuffer.subscribe())
|
||
});
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.add_diff(diff, cx);
|
||
multibuffer.expand_diff_hunks(vec![Anchor::Min..Anchor::Max], cx);
|
||
});
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc! {
|
||
" one
|
||
- two
|
||
- three
|
||
four
|
||
- five
|
||
- six
|
||
seven
|
||
- eight
|
||
"
|
||
},
|
||
);
|
||
|
||
assert_eq!(
|
||
snapshot
|
||
.diff_hunks_in_range(Point::new(1, 0)..Point::MAX)
|
||
.map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0)
|
||
.collect::<Vec<_>>(),
|
||
vec![1..3, 4..6, 7..8]
|
||
);
|
||
|
||
assert_eq!(snapshot.diff_hunk_before(Point::new(1, 1)), None,);
|
||
assert_eq!(
|
||
snapshot.diff_hunk_before(Point::new(7, 0)),
|
||
Some(MultiBufferRow(4))
|
||
);
|
||
assert_eq!(
|
||
snapshot.diff_hunk_before(Point::new(4, 0)),
|
||
Some(MultiBufferRow(1))
|
||
);
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.collapse_diff_hunks(vec![Anchor::Min..Anchor::Max], cx);
|
||
});
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc! {
|
||
"
|
||
one
|
||
four
|
||
seven
|
||
"
|
||
},
|
||
);
|
||
|
||
assert_eq!(
|
||
snapshot.diff_hunk_before(Point::new(2, 0)),
|
||
Some(MultiBufferRow(1)),
|
||
);
|
||
assert_eq!(
|
||
snapshot.diff_hunk_before(Point::new(4, 0)),
|
||
Some(MultiBufferRow(2))
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_diff_hunks_in_range_query_starting_at_added_row(cx: &mut TestAppContext) {
|
||
let base_text = "one\ntwo\nthree\n";
|
||
let text = "one\nTWO\nthree\n";
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||
let diff = cx
|
||
.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||
(multibuffer.snapshot(cx), multibuffer.subscribe())
|
||
});
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.add_diff(diff, cx);
|
||
multibuffer.expand_diff_hunks(vec![Anchor::Min..Anchor::Max], cx);
|
||
});
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc! {
|
||
" one
|
||
- two
|
||
+ TWO
|
||
three
|
||
"
|
||
},
|
||
);
|
||
|
||
assert_eq!(
|
||
snapshot
|
||
.diff_hunks_in_range(Point::new(2, 0)..Point::MAX)
|
||
.map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0)
|
||
.collect::<Vec<_>>(),
|
||
vec![1..3],
|
||
"querying starting at the added row should still return the full hunk including deleted lines"
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_inverted_diff_hunks_in_range(cx: &mut TestAppContext) {
|
||
let base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n";
|
||
let text = "ZERO\none\nTHREE\nfour\nseven\nEIGHT\nNINE\n";
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||
let diff = cx
|
||
.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
|
||
let base_text_buffer = diff.read_with(cx, |diff, _| diff.base_text_buffer().clone());
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(base_text_buffer.clone(), cx));
|
||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||
(multibuffer.snapshot(cx), multibuffer.subscribe())
|
||
});
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.add_inverted_diff(diff, buffer.clone(), cx);
|
||
});
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc! {
|
||
" one
|
||
- two
|
||
- three
|
||
four
|
||
- five
|
||
- six
|
||
seven
|
||
- eight
|
||
"
|
||
},
|
||
);
|
||
|
||
assert_eq!(
|
||
snapshot
|
||
.diff_hunks_in_range(Point::new(0, 0)..Point::MAX)
|
||
.map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0)
|
||
.collect::<Vec<_>>(),
|
||
vec![0..0, 1..3, 4..6, 7..8]
|
||
);
|
||
|
||
assert_eq!(
|
||
snapshot.diff_hunk_before(Point::new(1, 1)),
|
||
Some(MultiBufferRow(0))
|
||
);
|
||
assert_eq!(
|
||
snapshot.diff_hunk_before(Point::new(7, 0)),
|
||
Some(MultiBufferRow(4))
|
||
);
|
||
assert_eq!(
|
||
snapshot.diff_hunk_before(Point::new(4, 0)),
|
||
Some(MultiBufferRow(1))
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) {
|
||
let base_text = "one\ntwo\nfour\nfive\nsix\nseven\n";
|
||
let text = "one\ntwo\nTHREE\nfour\nfive\nseven\n";
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||
let diff = cx
|
||
.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||
|
||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.add_diff(diff.clone(), cx);
|
||
(multibuffer.snapshot(cx), multibuffer.subscribe())
|
||
});
|
||
|
||
cx.executor().run_until_parked();
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||
});
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc! {
|
||
"
|
||
one
|
||
two
|
||
+ THREE
|
||
four
|
||
five
|
||
- six
|
||
seven
|
||
"
|
||
},
|
||
);
|
||
|
||
// Insert a newline within an insertion hunk
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.edit([(Point::new(2, 0)..Point::new(2, 0), "__\n__")], None, cx);
|
||
});
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc! {
|
||
"
|
||
one
|
||
two
|
||
+ __
|
||
+ __THREE
|
||
four
|
||
five
|
||
- six
|
||
seven
|
||
"
|
||
},
|
||
);
|
||
|
||
// Delete the newline before a deleted hunk.
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.edit([(Point::new(5, 4)..Point::new(6, 0), "")], None, cx);
|
||
});
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc! {
|
||
"
|
||
one
|
||
two
|
||
+ __
|
||
+ __THREE
|
||
four
|
||
fiveseven
|
||
"
|
||
},
|
||
);
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| multibuffer.undo(cx));
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc! {
|
||
"
|
||
one
|
||
two
|
||
+ __
|
||
+ __THREE
|
||
four
|
||
five
|
||
- six
|
||
seven
|
||
"
|
||
},
|
||
);
|
||
|
||
// Cannot (yet) insert at the beginning of a deleted hunk.
|
||
// (because it would put the newline in the wrong place)
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.edit([(Point::new(6, 0)..Point::new(6, 0), "\n")], None, cx);
|
||
});
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc! {
|
||
"
|
||
one
|
||
two
|
||
+ __
|
||
+ __THREE
|
||
four
|
||
five
|
||
- six
|
||
seven
|
||
"
|
||
},
|
||
);
|
||
|
||
// Replace a range that ends in a deleted hunk.
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.edit([(Point::new(5, 2)..Point::new(6, 2), "fty-")], None, cx);
|
||
});
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc! {
|
||
"
|
||
one
|
||
two
|
||
+ __
|
||
+ __THREE
|
||
four
|
||
fifty-seven
|
||
"
|
||
},
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_excerpt_events(cx: &mut App) {
|
||
let buffer_1 = cx.new(|cx| Buffer::local(sample_text(10, 3, 'a'), cx));
|
||
let buffer_2 = cx.new(|cx| Buffer::local(sample_text(10, 3, 'm'), cx));
|
||
|
||
let leader_multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
let follower_multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
let follower_edit_event_count = Arc::new(RwLock::new(0));
|
||
|
||
follower_multibuffer.update(cx, |_, cx| {
|
||
let follower_edit_event_count = follower_edit_event_count.clone();
|
||
cx.subscribe(
|
||
&leader_multibuffer,
|
||
move |follower, _, event, cx| match event.clone() {
|
||
Event::BufferRangesUpdated {
|
||
buffer,
|
||
path_key,
|
||
ranges,
|
||
} => {
|
||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||
follower.set_merged_excerpt_ranges_for_path(
|
||
path_key,
|
||
buffer,
|
||
&buffer_snapshot,
|
||
ranges,
|
||
cx,
|
||
);
|
||
}
|
||
Event::BuffersRemoved {
|
||
removed_buffer_ids, ..
|
||
} => {
|
||
for id in removed_buffer_ids {
|
||
follower.remove_excerpts_for_buffer(id, cx);
|
||
}
|
||
}
|
||
Event::Edited { .. } => {
|
||
*follower_edit_event_count.write() += 1;
|
||
}
|
||
_ => {}
|
||
},
|
||
)
|
||
.detach();
|
||
});
|
||
|
||
let buffer_1_snapshot = buffer_1.read(cx).snapshot();
|
||
let buffer_2_snapshot = buffer_2.read(cx).snapshot();
|
||
leader_multibuffer.update(cx, |leader, cx| {
|
||
leader.set_excerpt_ranges_for_path(
|
||
PathKey::sorted(0),
|
||
buffer_1.clone(),
|
||
&buffer_1_snapshot,
|
||
vec![
|
||
ExcerptRange::new((0..8).to_point(&buffer_1_snapshot)),
|
||
ExcerptRange::new((22..26).to_point(&buffer_1_snapshot)),
|
||
],
|
||
cx,
|
||
);
|
||
leader.set_excerpt_ranges_for_path(
|
||
PathKey::sorted(1),
|
||
buffer_2.clone(),
|
||
&buffer_2_snapshot,
|
||
vec![
|
||
ExcerptRange::new((0..5).to_point(&buffer_2_snapshot)),
|
||
ExcerptRange::new((20..25).to_point(&buffer_2_snapshot)),
|
||
],
|
||
cx,
|
||
);
|
||
});
|
||
assert_eq!(
|
||
leader_multibuffer.read(cx).snapshot(cx).text(),
|
||
follower_multibuffer.read(cx).snapshot(cx).text(),
|
||
);
|
||
assert_eq!(*follower_edit_event_count.read(), 2);
|
||
|
||
leader_multibuffer.update(cx, |leader, cx| {
|
||
leader.set_excerpt_ranges_for_path(
|
||
PathKey::sorted(0),
|
||
buffer_1.clone(),
|
||
&buffer_1_snapshot,
|
||
vec![ExcerptRange::new((0..8).to_point(&buffer_1_snapshot))],
|
||
cx,
|
||
);
|
||
leader.set_excerpt_ranges_for_path(
|
||
PathKey::sorted(1),
|
||
buffer_2,
|
||
&buffer_2_snapshot,
|
||
vec![ExcerptRange::new((0..5).to_point(&buffer_2_snapshot))],
|
||
cx,
|
||
);
|
||
});
|
||
assert_eq!(
|
||
leader_multibuffer.read(cx).snapshot(cx).text(),
|
||
follower_multibuffer.read(cx).snapshot(cx).text(),
|
||
);
|
||
assert_eq!(*follower_edit_event_count.read(), 4);
|
||
|
||
leader_multibuffer.update(cx, |leader, cx| {
|
||
leader.clear(cx);
|
||
});
|
||
assert_eq!(
|
||
leader_multibuffer.read(cx).snapshot(cx).text(),
|
||
follower_multibuffer.read(cx).snapshot(cx).text(),
|
||
);
|
||
assert_eq!(*follower_edit_event_count.read(), 5);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_expand_excerpts(cx: &mut App) {
|
||
let buffer = cx.new(|cx| Buffer::local(sample_text(20, 3, 'a'), cx));
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::for_buffer(&buffer, cx),
|
||
buffer,
|
||
vec![
|
||
// Note that in this test, this first excerpt
|
||
// does not contain a new line
|
||
Point::new(3, 2)..Point::new(3, 3),
|
||
Point::new(7, 1)..Point::new(7, 3),
|
||
Point::new(15, 0)..Point::new(15, 0),
|
||
],
|
||
1,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
|
||
assert_eq!(
|
||
snapshot.text(),
|
||
concat!(
|
||
"ccc\n", //
|
||
"ddd\n", //
|
||
"eee", //
|
||
"\n", // End of excerpt
|
||
"ggg\n", //
|
||
"hhh\n", //
|
||
"iii", //
|
||
"\n", // End of excerpt
|
||
"ooo\n", //
|
||
"ppp\n", //
|
||
"qqq", // End of excerpt
|
||
)
|
||
);
|
||
drop(snapshot);
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
let multibuffer_snapshot = multibuffer.snapshot(cx);
|
||
let line_zero = multibuffer_snapshot.anchor_before(Point::new(0, 0));
|
||
multibuffer.expand_excerpts(
|
||
multibuffer.snapshot(cx).excerpts().map(|excerpt| {
|
||
multibuffer_snapshot
|
||
.anchor_in_excerpt(excerpt.context.start)
|
||
.unwrap()
|
||
}),
|
||
1,
|
||
ExpandExcerptDirection::UpAndDown,
|
||
cx,
|
||
);
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
let line_two = snapshot.anchor_before(Point::new(2, 0));
|
||
assert_eq!(line_two.cmp(&line_zero, &snapshot), cmp::Ordering::Greater);
|
||
});
|
||
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
|
||
assert_eq!(
|
||
snapshot.text(),
|
||
concat!(
|
||
"bbb\n", //
|
||
"ccc\n", //
|
||
"ddd\n", //
|
||
"eee\n", //
|
||
"fff\n", //
|
||
"ggg\n", //
|
||
"hhh\n", //
|
||
"iii\n", //
|
||
"jjj\n", // End of excerpt
|
||
"nnn\n", //
|
||
"ooo\n", //
|
||
"ppp\n", //
|
||
"qqq\n", //
|
||
"rrr", // End of excerpt
|
||
)
|
||
);
|
||
}
|
||
|
||
#[gpui::test(iterations = 100)]
|
||
async fn test_set_anchored_excerpts_for_path(cx: &mut TestAppContext) {
|
||
let buffer_1 = cx.new(|cx| Buffer::local(sample_text(20, 3, 'a'), cx));
|
||
let buffer_2 = cx.new(|cx| Buffer::local(sample_text(15, 4, 'a'), cx));
|
||
let snapshot_1 = buffer_1.update(cx, |buffer, _| buffer.snapshot());
|
||
let snapshot_2 = buffer_2.update(cx, |buffer, _| buffer.snapshot());
|
||
let ranges_1 = vec![
|
||
snapshot_1.anchor_before(Point::new(3, 2))..snapshot_1.anchor_before(Point::new(4, 2)),
|
||
snapshot_1.anchor_before(Point::new(7, 1))..snapshot_1.anchor_before(Point::new(7, 3)),
|
||
snapshot_1.anchor_before(Point::new(15, 0))..snapshot_1.anchor_before(Point::new(15, 0)),
|
||
];
|
||
let ranges_2 = vec![
|
||
snapshot_2.anchor_before(Point::new(2, 1))..snapshot_2.anchor_before(Point::new(3, 1)),
|
||
snapshot_2.anchor_before(Point::new(10, 0))..snapshot_2.anchor_before(Point::new(10, 2)),
|
||
];
|
||
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
let anchor_ranges_1 = multibuffer
|
||
.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_anchored_excerpts_for_path(
|
||
PathKey::for_buffer(&buffer_1, cx),
|
||
buffer_1.clone(),
|
||
ranges_1,
|
||
2,
|
||
cx,
|
||
)
|
||
})
|
||
.await;
|
||
let snapshot_1 = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||
assert_eq!(
|
||
anchor_ranges_1
|
||
.iter()
|
||
.map(|range| range.to_point(&snapshot_1))
|
||
.collect::<Vec<_>>(),
|
||
vec![
|
||
Point::new(2, 2)..Point::new(3, 2),
|
||
Point::new(6, 1)..Point::new(6, 3),
|
||
Point::new(11, 0)..Point::new(11, 0),
|
||
]
|
||
);
|
||
let anchor_ranges_2 = multibuffer
|
||
.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_anchored_excerpts_for_path(
|
||
PathKey::for_buffer(&buffer_2, cx),
|
||
buffer_2.clone(),
|
||
ranges_2,
|
||
2,
|
||
cx,
|
||
)
|
||
})
|
||
.await;
|
||
let snapshot_2 = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||
assert_eq!(
|
||
anchor_ranges_2
|
||
.iter()
|
||
.map(|range| range.to_point(&snapshot_2))
|
||
.collect::<Vec<_>>(),
|
||
vec![
|
||
Point::new(16, 1)..Point::new(17, 1),
|
||
Point::new(22, 0)..Point::new(22, 2)
|
||
]
|
||
);
|
||
|
||
let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||
assert_eq!(
|
||
snapshot.text(),
|
||
concat!(
|
||
"bbb\n", // buffer_1
|
||
"ccc\n", //
|
||
"ddd\n", // <-- excerpt 1
|
||
"eee\n", // <-- excerpt 1
|
||
"fff\n", //
|
||
"ggg\n", //
|
||
"hhh\n", // <-- excerpt 2
|
||
"iii\n", //
|
||
"jjj\n", //
|
||
//
|
||
"nnn\n", //
|
||
"ooo\n", //
|
||
"ppp\n", // <-- excerpt 3
|
||
"qqq\n", //
|
||
"rrr\n", //
|
||
//
|
||
"aaaa\n", // buffer 2
|
||
"bbbb\n", //
|
||
"cccc\n", // <-- excerpt 4
|
||
"dddd\n", // <-- excerpt 4
|
||
"eeee\n", //
|
||
"ffff\n", //
|
||
//
|
||
"iiii\n", //
|
||
"jjjj\n", //
|
||
"kkkk\n", // <-- excerpt 5
|
||
"llll\n", //
|
||
"mmmm", //
|
||
)
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_empty_multibuffer(cx: &mut App) {
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
assert_eq!(snapshot.text(), "");
|
||
assert_eq!(
|
||
snapshot
|
||
.row_infos(MultiBufferRow(0))
|
||
.map(|info| info.buffer_row)
|
||
.collect::<Vec<_>>(),
|
||
&[Some(0)]
|
||
);
|
||
assert!(
|
||
snapshot
|
||
.row_infos(MultiBufferRow(1))
|
||
.map(|info| info.buffer_row)
|
||
.collect::<Vec<_>>()
|
||
.is_empty(),
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_empty_diff_excerpt(cx: &mut TestAppContext) {
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||
let base_text = "a\nb\nc";
|
||
|
||
let diff = cx
|
||
.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpt_ranges_for_path(
|
||
PathKey::sorted(0),
|
||
buffer.clone(),
|
||
&buffer.read(cx).snapshot(),
|
||
vec![ExcerptRange::new(Point::zero()..Point::zero())],
|
||
cx,
|
||
);
|
||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||
multibuffer.add_diff(diff.clone(), cx);
|
||
});
|
||
cx.run_until_parked();
|
||
|
||
let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||
assert_eq!(snapshot.text(), "a\nb\nc\n");
|
||
|
||
let hunk = snapshot
|
||
.diff_hunks_in_range(Point::new(1, 1)..Point::new(1, 1))
|
||
.next()
|
||
.unwrap();
|
||
|
||
assert_eq!(hunk.diff_base_byte_range.start, BufferOffset(0));
|
||
|
||
let buf2 = cx.new(|cx| Buffer::local("X", cx));
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(1),
|
||
buf2,
|
||
[Point::new(0, 0)..Point::new(0, 1)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
buffer.update(cx, |buffer, cx| {
|
||
buffer.edit([(0..0, "a\nb\nc")], None, cx);
|
||
diff.update(cx, |diff, cx| {
|
||
diff.recalculate_diff_sync(&buffer.text_snapshot(), cx);
|
||
});
|
||
assert_eq!(buffer.text(), "a\nb\nc")
|
||
});
|
||
cx.run_until_parked();
|
||
|
||
let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||
assert_eq!(snapshot.text(), "a\nb\nc\nX");
|
||
|
||
buffer.update(cx, |buffer, cx| {
|
||
buffer.undo(cx);
|
||
diff.update(cx, |diff, cx| {
|
||
diff.recalculate_diff_sync(&buffer.text_snapshot(), cx);
|
||
});
|
||
assert_eq!(buffer.text(), "")
|
||
});
|
||
cx.run_until_parked();
|
||
|
||
let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||
assert_eq!(snapshot.text(), "a\nb\nc\n\nX");
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_singleton_multibuffer_anchors(cx: &mut App) {
|
||
let buffer = cx.new(|cx| Buffer::local("abcd", cx));
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||
let old_snapshot = multibuffer.read(cx).snapshot(cx);
|
||
buffer.update(cx, |buffer, cx| {
|
||
buffer.edit([(0..0, "X")], None, cx);
|
||
buffer.edit([(5..5, "Y")], None, cx);
|
||
});
|
||
let new_snapshot = multibuffer.read(cx).snapshot(cx);
|
||
|
||
assert_eq!(old_snapshot.text(), "abcd");
|
||
assert_eq!(new_snapshot.text(), "XabcdY");
|
||
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_before(MultiBufferOffset(0))
|
||
.to_offset(&new_snapshot),
|
||
MultiBufferOffset(0)
|
||
);
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_after(MultiBufferOffset(0))
|
||
.to_offset(&new_snapshot),
|
||
MultiBufferOffset(1)
|
||
);
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_before(MultiBufferOffset(4))
|
||
.to_offset(&new_snapshot),
|
||
MultiBufferOffset(5)
|
||
);
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_after(MultiBufferOffset(4))
|
||
.to_offset(&new_snapshot),
|
||
MultiBufferOffset(6)
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_multibuffer_anchors(cx: &mut App) {
|
||
let buffer_1 = cx.new(|cx| Buffer::local("abcd", cx));
|
||
let buffer_2 = cx.new(|cx| Buffer::local("efghi", cx));
|
||
let multibuffer = cx.new(|cx| {
|
||
let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(0),
|
||
buffer_1.clone(),
|
||
[Point::new(0, 0)..Point::new(0, 4)],
|
||
0,
|
||
cx,
|
||
);
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(1),
|
||
buffer_2.clone(),
|
||
[Point::new(0, 0)..Point::new(0, 5)],
|
||
0,
|
||
cx,
|
||
);
|
||
multibuffer
|
||
});
|
||
let old_snapshot = multibuffer.read(cx).snapshot(cx);
|
||
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_before(MultiBufferOffset(0))
|
||
.to_offset(&old_snapshot),
|
||
MultiBufferOffset(0)
|
||
);
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_after(MultiBufferOffset(0))
|
||
.to_offset(&old_snapshot),
|
||
MultiBufferOffset(0)
|
||
);
|
||
assert_eq!(Anchor::Min.to_offset(&old_snapshot), MultiBufferOffset(0));
|
||
assert_eq!(Anchor::Min.to_offset(&old_snapshot), MultiBufferOffset(0));
|
||
assert_eq!(Anchor::Max.to_offset(&old_snapshot), MultiBufferOffset(10));
|
||
assert_eq!(Anchor::Max.to_offset(&old_snapshot), MultiBufferOffset(10));
|
||
|
||
buffer_1.update(cx, |buffer, cx| {
|
||
buffer.edit([(0..0, "W")], None, cx);
|
||
buffer.edit([(5..5, "X")], None, cx);
|
||
});
|
||
buffer_2.update(cx, |buffer, cx| {
|
||
buffer.edit([(0..0, "Y")], None, cx);
|
||
buffer.edit([(6..6, "Z")], None, cx);
|
||
});
|
||
let new_snapshot = multibuffer.read(cx).snapshot(cx);
|
||
|
||
assert_eq!(old_snapshot.text(), "abcd\nefghi");
|
||
assert_eq!(new_snapshot.text(), "WabcdX\nYefghiZ");
|
||
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_before(MultiBufferOffset(0))
|
||
.to_offset(&new_snapshot),
|
||
MultiBufferOffset(0)
|
||
);
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_after(MultiBufferOffset(0))
|
||
.to_offset(&new_snapshot),
|
||
MultiBufferOffset(1)
|
||
);
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_before(MultiBufferOffset(1))
|
||
.to_offset(&new_snapshot),
|
||
MultiBufferOffset(2)
|
||
);
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_after(MultiBufferOffset(1))
|
||
.to_offset(&new_snapshot),
|
||
MultiBufferOffset(2)
|
||
);
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_before(MultiBufferOffset(2))
|
||
.to_offset(&new_snapshot),
|
||
MultiBufferOffset(3)
|
||
);
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_after(MultiBufferOffset(2))
|
||
.to_offset(&new_snapshot),
|
||
MultiBufferOffset(3)
|
||
);
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_before(MultiBufferOffset(5))
|
||
.to_offset(&new_snapshot),
|
||
MultiBufferOffset(7)
|
||
);
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_after(MultiBufferOffset(5))
|
||
.to_offset(&new_snapshot),
|
||
MultiBufferOffset(8)
|
||
);
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_before(MultiBufferOffset(10))
|
||
.to_offset(&new_snapshot),
|
||
MultiBufferOffset(13)
|
||
);
|
||
assert_eq!(
|
||
old_snapshot
|
||
.anchor_after(MultiBufferOffset(10))
|
||
.to_offset(&new_snapshot),
|
||
MultiBufferOffset(14)
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_basic_diff_hunks(cx: &mut TestAppContext) {
|
||
let text = indoc!(
|
||
"
|
||
ZERO
|
||
one
|
||
TWO
|
||
three
|
||
six
|
||
"
|
||
);
|
||
let base_text = indoc!(
|
||
"
|
||
one
|
||
two
|
||
three
|
||
four
|
||
five
|
||
six
|
||
"
|
||
);
|
||
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||
let diff = cx
|
||
.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
|
||
cx.run_until_parked();
|
||
|
||
let multibuffer = cx.new(|cx| {
|
||
let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
|
||
multibuffer.add_diff(diff.clone(), cx);
|
||
multibuffer
|
||
});
|
||
|
||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||
(multibuffer.snapshot(cx), multibuffer.subscribe())
|
||
});
|
||
assert_eq!(
|
||
snapshot.text(),
|
||
indoc!(
|
||
"
|
||
ZERO
|
||
one
|
||
TWO
|
||
three
|
||
six
|
||
"
|
||
),
|
||
);
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.expand_diff_hunks(vec![Anchor::Min..Anchor::Max], cx);
|
||
});
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
+ ZERO
|
||
one
|
||
- two
|
||
+ TWO
|
||
three
|
||
- four
|
||
- five
|
||
six
|
||
"
|
||
),
|
||
);
|
||
|
||
assert_eq!(
|
||
snapshot
|
||
.row_infos(MultiBufferRow(0))
|
||
.map(|info| (info.buffer_row, info.diff_status))
|
||
.collect::<Vec<_>>(),
|
||
vec![
|
||
(Some(0), Some(DiffHunkStatus::added_none())),
|
||
(Some(1), None),
|
||
(Some(1), Some(DiffHunkStatus::deleted_none())),
|
||
(Some(2), Some(DiffHunkStatus::added_none())),
|
||
(Some(3), None),
|
||
(Some(3), Some(DiffHunkStatus::deleted_none())),
|
||
(Some(4), Some(DiffHunkStatus::deleted_none())),
|
||
(Some(4), None),
|
||
(Some(5), None)
|
||
]
|
||
);
|
||
|
||
assert_chunks_in_ranges(&snapshot);
|
||
assert_consistent_line_numbers(&snapshot);
|
||
assert_position_translation(&snapshot);
|
||
assert_line_indents(&snapshot);
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.collapse_diff_hunks(vec![Anchor::Min..Anchor::Max], cx)
|
||
});
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
ZERO
|
||
one
|
||
TWO
|
||
three
|
||
six
|
||
"
|
||
),
|
||
);
|
||
|
||
assert_chunks_in_ranges(&snapshot);
|
||
assert_consistent_line_numbers(&snapshot);
|
||
assert_position_translation(&snapshot);
|
||
assert_line_indents(&snapshot);
|
||
|
||
// Expand the first diff hunk
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
let position = multibuffer.read(cx).anchor_before(Point::new(2, 2));
|
||
multibuffer.expand_diff_hunks(vec![position..position], cx)
|
||
});
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
ZERO
|
||
one
|
||
- two
|
||
+ TWO
|
||
three
|
||
six
|
||
"
|
||
),
|
||
);
|
||
|
||
// Expand the second diff hunk
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
let start = multibuffer.read(cx).anchor_before(Point::new(4, 0));
|
||
let end = multibuffer.read(cx).anchor_before(Point::new(5, 0));
|
||
multibuffer.expand_diff_hunks(vec![start..end], cx)
|
||
});
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
ZERO
|
||
one
|
||
- two
|
||
+ TWO
|
||
three
|
||
- four
|
||
- five
|
||
six
|
||
"
|
||
),
|
||
);
|
||
|
||
assert_chunks_in_ranges(&snapshot);
|
||
assert_consistent_line_numbers(&snapshot);
|
||
assert_position_translation(&snapshot);
|
||
assert_line_indents(&snapshot);
|
||
|
||
// Edit the buffer before the first hunk
|
||
buffer.update(cx, |buffer, cx| {
|
||
buffer.edit_via_marked_text(
|
||
indoc!(
|
||
"
|
||
ZERO
|
||
one« hundred
|
||
thousand»
|
||
TWO
|
||
three
|
||
six
|
||
"
|
||
),
|
||
None,
|
||
cx,
|
||
);
|
||
});
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
ZERO
|
||
one hundred
|
||
thousand
|
||
- two
|
||
+ TWO
|
||
three
|
||
- four
|
||
- five
|
||
six
|
||
"
|
||
),
|
||
);
|
||
|
||
assert_chunks_in_ranges(&snapshot);
|
||
assert_consistent_line_numbers(&snapshot);
|
||
assert_position_translation(&snapshot);
|
||
assert_line_indents(&snapshot);
|
||
|
||
// Recalculate the diff, changing the first diff hunk.
|
||
diff.update(cx, |diff, cx| {
|
||
diff.recalculate_diff_sync(&buffer.read(cx).text_snapshot(), cx);
|
||
});
|
||
cx.run_until_parked();
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
ZERO
|
||
one hundred
|
||
thousand
|
||
TWO
|
||
three
|
||
- four
|
||
- five
|
||
six
|
||
"
|
||
),
|
||
);
|
||
|
||
assert_eq!(
|
||
snapshot
|
||
.diff_hunks_in_range(MultiBufferOffset(0)..snapshot.len())
|
||
.map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0)
|
||
.collect::<Vec<_>>(),
|
||
&[0..4, 5..7]
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_text_for_range_with_diff_transform_boundary_inside_multibyte_character(cx: &mut App) {
|
||
let buffer = cx.new(|cx| Buffer::local("タx", cx));
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let mut snapshot = multibuffer.read(cx).snapshot(cx);
|
||
|
||
fn ascii_summary_with_byte_len(byte_len: usize) -> MBTextSummary {
|
||
let text = "x".repeat(byte_len);
|
||
MBTextSummary::from(TextSummary::from(text.as_str()))
|
||
}
|
||
|
||
// FR-16 shown a diff transform boundary two bytes into the leading 'タ'.
|
||
// Build that transform tree directly so this test stays focused on chunk iteration.
|
||
let mut diff_transforms = SumTree::default();
|
||
diff_transforms.push(
|
||
DiffTransform::BufferContent {
|
||
summary: ascii_summary_with_byte_len(2),
|
||
inserted_hunk_info: None,
|
||
},
|
||
(),
|
||
);
|
||
diff_transforms.push(
|
||
DiffTransform::BufferContent {
|
||
summary: ascii_summary_with_byte_len("タx".len() - 2),
|
||
inserted_hunk_info: None,
|
||
},
|
||
(),
|
||
);
|
||
snapshot.diff_transforms = diff_transforms;
|
||
|
||
let text = snapshot
|
||
.text_for_range(MultiBufferOffset(0)..snapshot.len())
|
||
.collect::<String>();
|
||
assert_eq!(text, "タx");
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_repeatedly_expand_a_diff_hunk(cx: &mut TestAppContext) {
|
||
let text = indoc!(
|
||
"
|
||
one
|
||
TWO
|
||
THREE
|
||
four
|
||
FIVE
|
||
six
|
||
"
|
||
);
|
||
let base_text = indoc!(
|
||
"
|
||
one
|
||
four
|
||
five
|
||
six
|
||
"
|
||
);
|
||
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||
let diff = cx
|
||
.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
|
||
cx.run_until_parked();
|
||
|
||
let multibuffer = cx.new(|cx| {
|
||
let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
|
||
multibuffer.add_diff(diff.clone(), cx);
|
||
multibuffer
|
||
});
|
||
|
||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||
(multibuffer.snapshot(cx), multibuffer.subscribe())
|
||
});
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.expand_diff_hunks(vec![Anchor::Min..Anchor::Max], cx);
|
||
});
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
one
|
||
+ TWO
|
||
+ THREE
|
||
four
|
||
- five
|
||
+ FIVE
|
||
six
|
||
"
|
||
),
|
||
);
|
||
|
||
// Regression test: expanding diff hunks that are already expanded should not change anything.
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.expand_diff_hunks(
|
||
vec![
|
||
snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_before(Point::new(2, 0)),
|
||
],
|
||
cx,
|
||
);
|
||
});
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
one
|
||
+ TWO
|
||
+ THREE
|
||
four
|
||
- five
|
||
+ FIVE
|
||
six
|
||
"
|
||
),
|
||
);
|
||
|
||
// Now collapse all diff hunks
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.collapse_diff_hunks(vec![Anchor::Min..Anchor::Max], cx);
|
||
});
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
one
|
||
TWO
|
||
THREE
|
||
four
|
||
FIVE
|
||
six
|
||
"
|
||
),
|
||
);
|
||
|
||
// Expand the hunks again, but this time provide two ranges that are both within the same hunk
|
||
// Target the first hunk which is between "one" and "four"
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.expand_diff_hunks(
|
||
vec![
|
||
snapshot.anchor_before(Point::new(4, 0))..snapshot.anchor_before(Point::new(4, 0)),
|
||
snapshot.anchor_before(Point::new(4, 2))..snapshot.anchor_before(Point::new(4, 2)),
|
||
],
|
||
cx,
|
||
);
|
||
});
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
one
|
||
TWO
|
||
THREE
|
||
four
|
||
- five
|
||
+ FIVE
|
||
six
|
||
"
|
||
),
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_set_excerpts_for_buffer_ordering(cx: &mut TestAppContext) {
|
||
let buf1 = cx.new(|cx| {
|
||
Buffer::local(
|
||
indoc! {
|
||
"zero
|
||
one
|
||
two
|
||
two.five
|
||
three
|
||
four
|
||
five
|
||
six
|
||
seven
|
||
eight
|
||
nine
|
||
ten
|
||
eleven
|
||
",
|
||
},
|
||
cx,
|
||
)
|
||
});
|
||
let path1: PathKey = PathKey::with_sort_prefix(0, rel_path("root").into_arc());
|
||
|
||
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(1..2),
|
||
Point::row_range(6..7),
|
||
Point::row_range(11..12),
|
||
],
|
||
1,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
assert_excerpts_match(
|
||
&multibuffer,
|
||
cx,
|
||
indoc! {
|
||
"-----
|
||
zero
|
||
one
|
||
two
|
||
two.five
|
||
-----
|
||
four
|
||
five
|
||
six
|
||
seven
|
||
-----
|
||
nine
|
||
ten
|
||
eleven
|
||
"
|
||
},
|
||
);
|
||
|
||
buf1.update(cx, |buffer, cx| buffer.edit([(0..5, "")], None, cx));
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
path1.clone(),
|
||
buf1.clone(),
|
||
vec![
|
||
Point::row_range(0..3),
|
||
Point::row_range(5..7),
|
||
Point::row_range(10..11),
|
||
],
|
||
1,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
assert_excerpts_match(
|
||
&multibuffer,
|
||
cx,
|
||
indoc! {
|
||
"-----
|
||
one
|
||
two
|
||
two.five
|
||
three
|
||
four
|
||
five
|
||
six
|
||
seven
|
||
eight
|
||
nine
|
||
ten
|
||
eleven
|
||
"
|
||
},
|
||
);
|
||
}
|
||
|
||
#[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: PathKey = PathKey::with_sort_prefix(0, rel_path("root").into_arc());
|
||
let buf2 = cx.new(|cx| {
|
||
Buffer::local(
|
||
indoc! {
|
||
"000
|
||
111
|
||
222
|
||
333
|
||
444
|
||
555
|
||
666
|
||
777
|
||
888
|
||
999
|
||
"
|
||
},
|
||
cx,
|
||
)
|
||
});
|
||
let path2 = PathKey::with_sort_prefix(1, rel_path("root").into_arc());
|
||
|
||
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_update_excerpt_ranges_for_path(cx: &mut TestAppContext) {
|
||
let buffer = cx.new(|cx| {
|
||
Buffer::local(
|
||
indoc! {
|
||
"row 0
|
||
row 1
|
||
row 2
|
||
row 3
|
||
row 4
|
||
row 5
|
||
row 6
|
||
row 7
|
||
row 8
|
||
row 9
|
||
row 10
|
||
row 11
|
||
row 12
|
||
row 13
|
||
row 14
|
||
"},
|
||
cx,
|
||
)
|
||
});
|
||
let path = PathKey::with_sort_prefix(0, rel_path("test.rs").into_arc());
|
||
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
path.clone(),
|
||
buffer.clone(),
|
||
vec![Point::row_range(2..4), Point::row_range(8..10)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
assert_excerpts_match(
|
||
&multibuffer,
|
||
cx,
|
||
indoc! {"-----
|
||
row 2
|
||
row 3
|
||
row 4
|
||
-----
|
||
row 8
|
||
row 9
|
||
row 10
|
||
"},
|
||
);
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.update_excerpts_for_path(
|
||
path.clone(),
|
||
buffer.clone(),
|
||
vec![Point::row_range(12..13)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
assert_excerpts_match(
|
||
&multibuffer,
|
||
cx,
|
||
indoc! {"-----
|
||
row 12
|
||
row 13
|
||
"},
|
||
);
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
path.clone(),
|
||
buffer.clone(),
|
||
vec![Point::row_range(2..4)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
assert_excerpts_match(
|
||
&multibuffer,
|
||
cx,
|
||
indoc! {"-----
|
||
row 2
|
||
row 3
|
||
row 4
|
||
"},
|
||
);
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.update_excerpts_for_path(
|
||
path.clone(),
|
||
buffer.clone(),
|
||
vec![Point::row_range(3..5)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
assert_excerpts_match(
|
||
&multibuffer,
|
||
cx,
|
||
indoc! {"-----
|
||
row 2
|
||
row 3
|
||
row 4
|
||
row 5
|
||
"},
|
||
);
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
path.clone(),
|
||
buffer.clone(),
|
||
vec![
|
||
Point::row_range(0..1),
|
||
Point::row_range(6..8),
|
||
Point::row_range(12..13),
|
||
],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
assert_excerpts_match(
|
||
&multibuffer,
|
||
cx,
|
||
indoc! {"-----
|
||
row 0
|
||
row 1
|
||
-----
|
||
row 6
|
||
row 7
|
||
row 8
|
||
-----
|
||
row 12
|
||
row 13
|
||
"},
|
||
);
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.update_excerpts_for_path(
|
||
path.clone(),
|
||
buffer.clone(),
|
||
vec![Point::row_range(7..9)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
assert_excerpts_match(
|
||
&multibuffer,
|
||
cx,
|
||
indoc! {"-----
|
||
row 6
|
||
row 7
|
||
row 8
|
||
row 9
|
||
"},
|
||
);
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
path.clone(),
|
||
buffer.clone(),
|
||
vec![Point::row_range(2..3), Point::row_range(6..7)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
assert_excerpts_match(
|
||
&multibuffer,
|
||
cx,
|
||
indoc! {"-----
|
||
row 2
|
||
row 3
|
||
-----
|
||
row 6
|
||
row 7
|
||
"},
|
||
);
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.update_excerpts_for_path(
|
||
path.clone(),
|
||
buffer.clone(),
|
||
vec![Point::row_range(3..6)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
assert_excerpts_match(
|
||
&multibuffer,
|
||
cx,
|
||
indoc! {"-----
|
||
row 2
|
||
row 3
|
||
row 4
|
||
row 5
|
||
row 6
|
||
row 7
|
||
"},
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_set_excerpts_for_buffer_rename(cx: &mut TestAppContext) {
|
||
let buf1 = cx.new(|cx| {
|
||
Buffer::local(
|
||
indoc! {
|
||
"zero
|
||
one
|
||
two
|
||
three
|
||
four
|
||
five
|
||
six
|
||
seven
|
||
",
|
||
},
|
||
cx,
|
||
)
|
||
});
|
||
let path: PathKey = PathKey::with_sort_prefix(0, rel_path("root").into_arc());
|
||
let buf2 = cx.new(|cx| {
|
||
Buffer::local(
|
||
indoc! {
|
||
"000
|
||
111
|
||
222
|
||
333
|
||
"
|
||
},
|
||
cx,
|
||
)
|
||
});
|
||
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
path.clone(),
|
||
buf1.clone(),
|
||
vec![Point::row_range(1..1), Point::row_range(4..5)],
|
||
1,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
assert_excerpts_match(
|
||
&multibuffer,
|
||
cx,
|
||
indoc! {
|
||
"-----
|
||
zero
|
||
one
|
||
two
|
||
three
|
||
four
|
||
five
|
||
six
|
||
"
|
||
},
|
||
);
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
path.clone(),
|
||
buf2.clone(),
|
||
vec![Point::row_range(0..1)],
|
||
2,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
assert_excerpts_match(
|
||
&multibuffer,
|
||
cx,
|
||
indoc! {"-----
|
||
000
|
||
111
|
||
222
|
||
333
|
||
"},
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_set_excerpts_for_path_replaces_previous_buffer(cx: &mut TestAppContext) {
|
||
let buffer_a = cx.new(|cx| {
|
||
Buffer::local(
|
||
indoc! {
|
||
"alpha
|
||
beta
|
||
gamma
|
||
delta
|
||
epsilon
|
||
",
|
||
},
|
||
cx,
|
||
)
|
||
});
|
||
let buffer_b = cx.new(|cx| {
|
||
Buffer::local(
|
||
indoc! {
|
||
"one
|
||
two
|
||
three
|
||
four
|
||
",
|
||
},
|
||
cx,
|
||
)
|
||
});
|
||
let path: PathKey = PathKey::with_sort_prefix(0, rel_path("shared/path").into_arc());
|
||
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
let removed_buffer_ids: Arc<RwLock<Vec<BufferId>>> = Default::default();
|
||
multibuffer.update(cx, |_, cx| {
|
||
let removed_buffer_ids = removed_buffer_ids.clone();
|
||
cx.subscribe(&multibuffer, move |_, _, event, _| {
|
||
if let Event::BuffersRemoved {
|
||
removed_buffer_ids: ids,
|
||
} = event
|
||
{
|
||
removed_buffer_ids.write().extend(ids.iter().copied());
|
||
}
|
||
})
|
||
.detach();
|
||
});
|
||
|
||
let ranges_a = vec![Point::row_range(0..1), Point::row_range(3..4)];
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(path.clone(), buffer_a.clone(), ranges_a.clone(), 0, cx);
|
||
});
|
||
let (anchor_a1, anchor_a2) = multibuffer.read_with(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
let buffer_snapshot = buffer_a.read(cx).snapshot();
|
||
let mut anchors = ranges_a.into_iter().filter_map(|range| {
|
||
let text_range = buffer_snapshot.anchor_range_inside(range);
|
||
let start = snapshot.anchor_in_buffer(text_range.start)?;
|
||
let end = snapshot.anchor_in_buffer(text_range.end)?;
|
||
Some(start..end)
|
||
});
|
||
(
|
||
anchors.next().expect("should have first anchor"),
|
||
anchors.next().expect("should have second anchor"),
|
||
)
|
||
});
|
||
|
||
assert_excerpts_match(
|
||
&multibuffer,
|
||
cx,
|
||
indoc! {
|
||
"-----
|
||
alpha
|
||
beta
|
||
-----
|
||
delta
|
||
epsilon
|
||
"
|
||
},
|
||
);
|
||
|
||
let buffer_a_id = buffer_a.read_with(cx, |buffer, _| buffer.remote_id());
|
||
multibuffer.read_with(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
assert!(
|
||
snapshot
|
||
.excerpts()
|
||
.any(|excerpt| excerpt.context.start.buffer_id == buffer_a_id),
|
||
);
|
||
});
|
||
|
||
let ranges_b = vec![Point::row_range(1..2)];
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(path.clone(), buffer_b.clone(), ranges_b.clone(), 1, cx);
|
||
});
|
||
let anchor_b = multibuffer.read_with(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
let buffer_snapshot = buffer_b.read(cx).snapshot();
|
||
ranges_b
|
||
.into_iter()
|
||
.filter_map(|range| {
|
||
let text_range = buffer_snapshot.anchor_range_inside(range);
|
||
let start = snapshot.anchor_in_buffer(text_range.start)?;
|
||
let end = snapshot.anchor_in_buffer(text_range.end)?;
|
||
Some(start..end)
|
||
})
|
||
.next()
|
||
.expect("should have an anchor")
|
||
});
|
||
|
||
let buffer_b_id = buffer_b.read_with(cx, |buffer, _| buffer.remote_id());
|
||
multibuffer.read_with(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
assert!(
|
||
!snapshot
|
||
.excerpts()
|
||
.any(|excerpt| excerpt.context.start.buffer_id == buffer_a_id),
|
||
);
|
||
assert!(
|
||
snapshot
|
||
.excerpts()
|
||
.any(|excerpt| excerpt.context.start.buffer_id == buffer_b_id),
|
||
);
|
||
assert!(
|
||
multibuffer.buffer(buffer_a_id).is_none(),
|
||
"old buffer should be fully removed from the multibuffer"
|
||
);
|
||
assert!(
|
||
multibuffer.buffer(buffer_b_id).is_some(),
|
||
"new buffer should be present in the multibuffer"
|
||
);
|
||
});
|
||
assert!(
|
||
removed_buffer_ids.read().contains(&buffer_a_id),
|
||
"BuffersRemoved event should have been emitted for the old buffer"
|
||
);
|
||
|
||
assert_excerpts_match(
|
||
&multibuffer,
|
||
cx,
|
||
indoc! {
|
||
"-----
|
||
one
|
||
two
|
||
three
|
||
four
|
||
"
|
||
},
|
||
);
|
||
|
||
multibuffer.read_with(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
anchor_a1.start.cmp(&anchor_b.start, &snapshot);
|
||
anchor_a1.end.cmp(&anchor_b.end, &snapshot);
|
||
anchor_a1.start.cmp(&anchor_a2.start, &snapshot);
|
||
anchor_a1.end.cmp(&anchor_a2.end, &snapshot);
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_stale_anchor_after_buffer_removal_and_path_reuse(cx: &mut TestAppContext) {
|
||
let buffer_a = cx.new(|cx| Buffer::local("aaa\nbbb\nccc\n", cx));
|
||
let buffer_b = cx.new(|cx| Buffer::local("xxx\nyyy\nzzz\n", cx));
|
||
let buffer_other = cx.new(|cx| Buffer::local("111\n222\n333\n", cx));
|
||
let path = PathKey::with_sort_prefix(0, rel_path("the/path").into_arc());
|
||
let other_path = PathKey::with_sort_prefix(1, rel_path("other/path").into_arc());
|
||
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
path.clone(),
|
||
buffer_a.clone(),
|
||
[Point::new(0, 0)..Point::new(2, 3)],
|
||
0,
|
||
cx,
|
||
);
|
||
multibuffer.set_excerpts_for_path(
|
||
other_path.clone(),
|
||
buffer_other.clone(),
|
||
[Point::new(0, 0)..Point::new(2, 3)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
buffer_a.update(cx, |buffer, cx| {
|
||
buffer.edit(
|
||
[(Point::new(1, 0)..Point::new(1, 0), "INSERTED ")],
|
||
None,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
let stale_anchor = multibuffer.read_with(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
snapshot.anchor_before(Point::new(1, 5))
|
||
});
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.remove_excerpts(path.clone(), cx);
|
||
});
|
||
|
||
multibuffer.read_with(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
let offset = stale_anchor.to_offset(&snapshot);
|
||
assert!(
|
||
offset.0 <= snapshot.len().0,
|
||
"stale anchor resolved to offset {offset:?} but multibuffer len is {:?}",
|
||
snapshot.len()
|
||
);
|
||
});
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
path.clone(),
|
||
buffer_b.clone(),
|
||
[Point::new(0, 0)..Point::new(2, 3)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
multibuffer.read_with(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
let offset = stale_anchor.to_offset(&snapshot);
|
||
assert!(
|
||
offset.0 <= snapshot.len().0,
|
||
"stale anchor resolved to offset {offset:?} but multibuffer len is {:?}",
|
||
snapshot.len()
|
||
);
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_map_excerpt_ranges(cx: &mut TestAppContext) {
|
||
let base_text = indoc!(
|
||
"
|
||
{
|
||
(aaa)
|
||
(bbb)
|
||
(ccc)
|
||
}
|
||
xxx
|
||
yyy
|
||
zzz
|
||
[
|
||
(ddd)
|
||
(EEE)
|
||
]
|
||
"
|
||
);
|
||
let text = indoc!(
|
||
"
|
||
{
|
||
(aaa)
|
||
(CCC)
|
||
}
|
||
xxx
|
||
yyy
|
||
zzz
|
||
[
|
||
(ddd)
|
||
(EEE)
|
||
]
|
||
"
|
||
);
|
||
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||
let diff = cx
|
||
.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
|
||
cx.run_until_parked();
|
||
|
||
let multibuffer = cx.new(|cx| {
|
||
let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(0),
|
||
buffer.clone(),
|
||
[
|
||
Point::new(0, 0)..Point::new(3, 1),
|
||
Point::new(7, 0)..Point::new(10, 1),
|
||
],
|
||
0,
|
||
cx,
|
||
);
|
||
multibuffer.add_diff(diff.clone(), cx);
|
||
multibuffer
|
||
});
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.expand_diff_hunks(vec![Anchor::Min..Anchor::Max], cx);
|
||
});
|
||
cx.run_until_parked();
|
||
|
||
let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||
|
||
let actual_diff = format_diff(
|
||
&snapshot.text(),
|
||
&snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>(),
|
||
&Default::default(),
|
||
None,
|
||
);
|
||
pretty_assertions::assert_eq!(
|
||
actual_diff,
|
||
indoc!(
|
||
"
|
||
{
|
||
(aaa)
|
||
- (bbb)
|
||
- (ccc)
|
||
+ (CCC)
|
||
} [\u{2193}]
|
||
[ [\u{2191}]
|
||
(ddd)
|
||
(EEE)
|
||
] [\u{2193}]"
|
||
)
|
||
);
|
||
|
||
assert_eq!(
|
||
snapshot.map_excerpt_ranges(
|
||
snapshot.point_to_offset(Point::new(1, 3))..snapshot.point_to_offset(Point::new(1, 3)),
|
||
|buffer, excerpt_range, input_range| {
|
||
assert_eq!(
|
||
buffer.offset_to_point(input_range.start.0)
|
||
..buffer.offset_to_point(input_range.end.0),
|
||
Point::new(1, 3)..Point::new(1, 3),
|
||
);
|
||
assert_eq!(
|
||
buffer.offset_to_point(excerpt_range.context.start.0)
|
||
..buffer.offset_to_point(excerpt_range.context.end.0),
|
||
Point::new(0, 0)..Point::new(3, 1),
|
||
);
|
||
vec![
|
||
(input_range.start..BufferOffset(input_range.start.0 + 3), ()),
|
||
(excerpt_range.context, ()),
|
||
(
|
||
BufferOffset(text::ToOffset::to_offset(&Point::new(2, 2), buffer))
|
||
..BufferOffset(text::ToOffset::to_offset(&Point::new(2, 7), buffer)),
|
||
(),
|
||
),
|
||
(
|
||
BufferOffset(text::ToOffset::to_offset(&Point::new(0, 0), buffer))
|
||
..BufferOffset(text::ToOffset::to_offset(&Point::new(2, 0), buffer)),
|
||
(),
|
||
),
|
||
]
|
||
},
|
||
),
|
||
Some(vec![
|
||
(
|
||
snapshot.point_to_offset(Point::new(1, 3))
|
||
..snapshot.point_to_offset(Point::new(1, 6)),
|
||
(),
|
||
),
|
||
(
|
||
snapshot.point_to_offset(Point::zero())..snapshot.point_to_offset(Point::new(5, 1)),
|
||
()
|
||
),
|
||
(
|
||
snapshot.point_to_offset(Point::new(4, 2))
|
||
..snapshot.point_to_offset(Point::new(4, 7)),
|
||
(),
|
||
),
|
||
(
|
||
snapshot.point_to_offset(Point::zero())..snapshot.point_to_offset(Point::new(4, 0)),
|
||
()
|
||
),
|
||
]),
|
||
);
|
||
|
||
assert_eq!(
|
||
snapshot.map_excerpt_ranges(
|
||
snapshot.point_to_offset(Point::new(5, 0))..snapshot.point_to_offset(Point::new(7, 0)),
|
||
|_, _, range| vec![(range, ())],
|
||
),
|
||
None,
|
||
);
|
||
|
||
assert_eq!(
|
||
snapshot.map_excerpt_ranges(
|
||
snapshot.point_to_offset(Point::new(7, 3))..snapshot.point_to_offset(Point::new(7, 6)),
|
||
|buffer, excerpt_range, input_range| {
|
||
assert_eq!(
|
||
buffer.offset_to_point(input_range.start.0)
|
||
..buffer.offset_to_point(input_range.end.0),
|
||
Point::new(8, 3)..Point::new(8, 6),
|
||
);
|
||
assert_eq!(
|
||
buffer.offset_to_point(excerpt_range.context.start.0)
|
||
..buffer.offset_to_point(excerpt_range.context.end.0),
|
||
Point::new(7, 0)..Point::new(10, 1),
|
||
);
|
||
vec![(input_range, ())]
|
||
},
|
||
),
|
||
Some(vec![(
|
||
snapshot.point_to_offset(Point::new(7, 3))..snapshot.point_to_offset(Point::new(7, 6)),
|
||
(),
|
||
)]),
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) {
|
||
let base_text_1 = indoc!(
|
||
"
|
||
one
|
||
two
|
||
three
|
||
four
|
||
five
|
||
six
|
||
"
|
||
);
|
||
let text_1 = indoc!(
|
||
"
|
||
ZERO
|
||
one
|
||
TWO
|
||
three
|
||
six
|
||
"
|
||
);
|
||
let base_text_2 = indoc!(
|
||
"
|
||
seven
|
||
eight
|
||
nine
|
||
ten
|
||
eleven
|
||
twelve
|
||
"
|
||
);
|
||
let text_2 = indoc!(
|
||
"
|
||
eight
|
||
nine
|
||
eleven
|
||
THIRTEEN
|
||
FOURTEEN
|
||
"
|
||
);
|
||
|
||
let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
|
||
let buffer_2 = cx.new(|cx| Buffer::local(text_2, cx));
|
||
let diff_1 = cx.new(|cx| {
|
||
BufferDiff::new_with_base_text(base_text_1, &buffer_1.read(cx).text_snapshot(), cx)
|
||
});
|
||
let diff_2 = cx.new(|cx| {
|
||
BufferDiff::new_with_base_text(base_text_2, &buffer_2.read(cx).text_snapshot(), cx)
|
||
});
|
||
cx.run_until_parked();
|
||
|
||
let multibuffer = cx.new(|cx| {
|
||
let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(0),
|
||
buffer_1.clone(),
|
||
[Point::zero()..buffer_1.read(cx).max_point()],
|
||
0,
|
||
cx,
|
||
);
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(1),
|
||
buffer_2.clone(),
|
||
[Point::zero()..buffer_2.read(cx).max_point()],
|
||
0,
|
||
cx,
|
||
);
|
||
multibuffer.add_diff(diff_1.clone(), cx);
|
||
multibuffer.add_diff(diff_2.clone(), cx);
|
||
multibuffer
|
||
});
|
||
|
||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||
(multibuffer.snapshot(cx), multibuffer.subscribe())
|
||
});
|
||
assert_eq!(
|
||
snapshot.text(),
|
||
indoc!(
|
||
"
|
||
ZERO
|
||
one
|
||
TWO
|
||
three
|
||
six
|
||
|
||
eight
|
||
nine
|
||
eleven
|
||
THIRTEEN
|
||
FOURTEEN
|
||
"
|
||
),
|
||
);
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.expand_diff_hunks(vec![Anchor::Min..Anchor::Max], cx);
|
||
});
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
+ ZERO
|
||
one
|
||
- two
|
||
+ TWO
|
||
three
|
||
- four
|
||
- five
|
||
six
|
||
|
||
- seven
|
||
eight
|
||
nine
|
||
- ten
|
||
eleven
|
||
- twelve
|
||
+ THIRTEEN
|
||
+ FOURTEEN
|
||
"
|
||
),
|
||
);
|
||
|
||
let id_1 = buffer_1.read_with(cx, |buffer, _| buffer.remote_id());
|
||
let id_2 = buffer_2.read_with(cx, |buffer, _| buffer.remote_id());
|
||
let base_id_1 = diff_1.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
|
||
let base_id_2 = diff_2.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
|
||
|
||
let buffer_lines = (0..=snapshot.max_row().0)
|
||
.map(|row| {
|
||
let (buffer, range) = snapshot.buffer_line_for_row(MultiBufferRow(row))?;
|
||
Some((
|
||
buffer.remote_id(),
|
||
buffer.text_for_range(range).collect::<String>(),
|
||
))
|
||
})
|
||
.collect::<Vec<_>>();
|
||
pretty_assertions::assert_eq!(
|
||
buffer_lines,
|
||
[
|
||
Some((id_1, "ZERO".into())),
|
||
Some((id_1, "one".into())),
|
||
Some((base_id_1, "two".into())),
|
||
Some((id_1, "TWO".into())),
|
||
Some((id_1, " three".into())),
|
||
Some((base_id_1, "four".into())),
|
||
Some((base_id_1, "five".into())),
|
||
Some((id_1, "six".into())),
|
||
Some((id_1, "".into())),
|
||
Some((base_id_2, "seven".into())),
|
||
Some((id_2, " eight".into())),
|
||
Some((id_2, "nine".into())),
|
||
Some((base_id_2, "ten".into())),
|
||
Some((id_2, "eleven".into())),
|
||
Some((base_id_2, "twelve".into())),
|
||
Some((id_2, "THIRTEEN".into())),
|
||
Some((id_2, "FOURTEEN".into())),
|
||
Some((id_2, "".into())),
|
||
]
|
||
);
|
||
|
||
let buffer_ids_by_range = [
|
||
(Point::new(0, 0)..Point::new(0, 0), &[id_1] as &[_]),
|
||
(Point::new(0, 0)..Point::new(2, 0), &[id_1]),
|
||
(Point::new(2, 0)..Point::new(2, 0), &[id_1]),
|
||
(Point::new(3, 0)..Point::new(3, 0), &[id_1]),
|
||
(Point::new(8, 0)..Point::new(9, 0), &[id_1]),
|
||
(Point::new(8, 0)..Point::new(10, 0), &[id_1, id_2]),
|
||
(Point::new(9, 0)..Point::new(9, 0), &[id_2]),
|
||
];
|
||
for (range, buffer_ids) in buffer_ids_by_range {
|
||
assert_eq!(
|
||
snapshot
|
||
.buffer_ids_for_range(range.clone())
|
||
.collect::<Vec<_>>(),
|
||
buffer_ids,
|
||
"buffer_ids_for_range({range:?}"
|
||
);
|
||
}
|
||
|
||
assert_position_translation(&snapshot);
|
||
assert_line_indents(&snapshot);
|
||
|
||
assert_eq!(
|
||
snapshot
|
||
.diff_hunks_in_range(MultiBufferOffset(0)..snapshot.len())
|
||
.map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0)
|
||
.collect::<Vec<_>>(),
|
||
&[0..1, 2..4, 5..7, 9..10, 12..13, 14..17]
|
||
);
|
||
|
||
buffer_2.update(cx, |buffer, cx| {
|
||
buffer.edit_via_marked_text(
|
||
indoc!(
|
||
"
|
||
eight
|
||
«»eleven
|
||
THIRTEEN
|
||
FOURTEEN
|
||
"
|
||
),
|
||
None,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
+ ZERO
|
||
one
|
||
- two
|
||
+ TWO
|
||
three
|
||
- four
|
||
- five
|
||
six
|
||
|
||
- seven
|
||
eight
|
||
eleven
|
||
- twelve
|
||
+ THIRTEEN
|
||
+ FOURTEEN
|
||
"
|
||
),
|
||
);
|
||
|
||
assert_line_indents(&snapshot);
|
||
}
|
||
|
||
/// A naive implementation of a multi-buffer that does not maintain
|
||
/// any derived state, used for comparison in a randomized test.
|
||
#[derive(Default)]
|
||
struct ReferenceMultibuffer {
|
||
excerpts: Vec<ReferenceExcerpt>,
|
||
diffs: HashMap<BufferId, Entity<BufferDiff>>,
|
||
inverted_diffs: HashMap<BufferId, (Entity<BufferDiff>, Entity<language::Buffer>)>,
|
||
expanded_diff_hunks_by_buffer: HashMap<BufferId, Vec<text::Anchor>>,
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
struct ReferenceExcerpt {
|
||
path_key: PathKey,
|
||
path_key_index: PathKeyIndex,
|
||
buffer: Entity<Buffer>,
|
||
range: Range<text::Anchor>,
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
struct ReferenceRegion {
|
||
buffer_id: Option<BufferId>,
|
||
range: Range<usize>,
|
||
buffer_range: Range<Point>,
|
||
// if this is a deleted hunk, the main buffer anchor to which the deleted content is attached
|
||
deleted_hunk_anchor: Option<text::Anchor>,
|
||
status: Option<DiffHunkStatus>,
|
||
excerpt: Option<ReferenceExcerpt>,
|
||
}
|
||
|
||
impl ReferenceMultibuffer {
|
||
fn expand_excerpts(
|
||
&mut self,
|
||
excerpts: &HashSet<ExcerptRange<text::Anchor>>,
|
||
line_count: u32,
|
||
cx: &mut App,
|
||
) {
|
||
use text::AnchorRangeExt as _;
|
||
|
||
if line_count == 0 || excerpts.is_empty() {
|
||
return;
|
||
}
|
||
|
||
let mut excerpts_by_buffer: HashMap<BufferId, Vec<ExcerptRange<text::Anchor>>> =
|
||
HashMap::default();
|
||
for excerpt in excerpts {
|
||
excerpts_by_buffer
|
||
.entry(excerpt.context.start.buffer_id)
|
||
.or_default()
|
||
.push(excerpt.clone())
|
||
}
|
||
|
||
for (buffer_id, excerpts_to_expand) in excerpts_by_buffer {
|
||
let mut buffer = None;
|
||
let mut buffer_snapshot = None;
|
||
let mut path = None;
|
||
let mut path_key_index = None;
|
||
let mut new_ranges =
|
||
self.excerpts
|
||
.iter()
|
||
.filter(|excerpt| excerpt.range.start.buffer_id == buffer_id)
|
||
.map(|excerpt| {
|
||
let snapshot = excerpt.buffer.read(cx).snapshot();
|
||
let mut range = excerpt.range.to_point(&snapshot);
|
||
if excerpts_to_expand.iter().any(|info| {
|
||
excerpt.range.contains_anchor(info.context.start, &snapshot)
|
||
}) {
|
||
range.start = Point::new(range.start.row.saturating_sub(line_count), 0);
|
||
range.end = snapshot
|
||
.clip_point(Point::new(range.end.row + line_count, 0), Bias::Left);
|
||
range.end.column = snapshot.line_len(range.end.row);
|
||
}
|
||
buffer = Some(excerpt.buffer.clone());
|
||
buffer_snapshot = Some(snapshot);
|
||
path = Some(excerpt.path_key.clone());
|
||
path_key_index = Some(excerpt.path_key_index);
|
||
ExcerptRange::new(range)
|
||
})
|
||
.collect::<Vec<_>>();
|
||
|
||
new_ranges.sort_by_key(|nr| nr.context.start);
|
||
|
||
self.set_excerpts(
|
||
path.unwrap(),
|
||
path_key_index.unwrap(),
|
||
buffer.unwrap(),
|
||
&buffer_snapshot.unwrap(),
|
||
new_ranges,
|
||
cx,
|
||
);
|
||
}
|
||
}
|
||
|
||
fn set_excerpts(
|
||
&mut self,
|
||
path_key: PathKey,
|
||
path_key_index: PathKeyIndex,
|
||
buffer: Entity<Buffer>,
|
||
buffer_snapshot: &BufferSnapshot,
|
||
ranges: Vec<ExcerptRange<Point>>,
|
||
cx: &mut App,
|
||
) {
|
||
self.excerpts.retain(|excerpt| {
|
||
excerpt.path_key != path_key && excerpt.buffer.entity_id() != buffer.entity_id()
|
||
});
|
||
|
||
let ranges = MultiBuffer::merge_excerpt_ranges(&ranges);
|
||
|
||
let (Ok(ix) | Err(ix)) = self
|
||
.excerpts
|
||
.binary_search_by(|probe| probe.path_key.cmp(&path_key));
|
||
self.excerpts.splice(
|
||
ix..ix,
|
||
ranges.into_iter().map(|range| ReferenceExcerpt {
|
||
path_key: path_key.clone(),
|
||
path_key_index,
|
||
buffer: buffer.clone(),
|
||
range: buffer_snapshot.anchor_before(range.context.start)
|
||
..buffer_snapshot.anchor_after(range.context.end),
|
||
}),
|
||
);
|
||
self.update_expanded_diff_hunks_for_buffer(buffer_snapshot.remote_id(), cx);
|
||
}
|
||
|
||
fn expand_diff_hunks(&mut self, path_key: PathKey, range: Range<text::Anchor>, cx: &App) {
|
||
let excerpt = self
|
||
.excerpts
|
||
.iter_mut()
|
||
.find(|e| {
|
||
e.path_key == path_key
|
||
&& e.range
|
||
.start
|
||
.cmp(&range.start, &e.buffer.read(cx).snapshot())
|
||
.is_le()
|
||
&& e.range
|
||
.end
|
||
.cmp(&range.end, &e.buffer.read(cx).snapshot())
|
||
.is_ge()
|
||
})
|
||
.unwrap();
|
||
let buffer = excerpt.buffer.read(cx).snapshot();
|
||
let buffer_id = buffer.remote_id();
|
||
|
||
// Skip inverted excerpts - hunks are always expanded
|
||
if self.inverted_diffs.contains_key(&buffer_id) {
|
||
return;
|
||
}
|
||
|
||
let Some(diff) = self.diffs.get(&buffer_id) else {
|
||
return;
|
||
};
|
||
let excerpt_range = excerpt.range.to_point(&buffer);
|
||
let expanded_diff_hunks = self
|
||
.expanded_diff_hunks_by_buffer
|
||
.entry(buffer_id)
|
||
.or_default();
|
||
for hunk in diff
|
||
.read(cx)
|
||
.snapshot(cx)
|
||
.hunks_intersecting_range(range, &buffer)
|
||
{
|
||
let hunk_range = hunk.buffer_range.to_point(&buffer);
|
||
if hunk_range.start < excerpt_range.start || hunk_range.start > excerpt_range.end {
|
||
continue;
|
||
}
|
||
if let Err(ix) = expanded_diff_hunks
|
||
.binary_search_by(|anchor| anchor.cmp(&hunk.buffer_range.start, &buffer))
|
||
{
|
||
log::info!(
|
||
"expanding diff hunk {:?}. excerpt range: {:?}, buffer {:?}",
|
||
hunk_range,
|
||
excerpt_range,
|
||
buffer.remote_id()
|
||
);
|
||
expanded_diff_hunks.insert(ix, hunk.buffer_range.start);
|
||
} else {
|
||
log::trace!("hunk {hunk_range:?} already expanded in excerpt");
|
||
}
|
||
}
|
||
}
|
||
|
||
fn expected_content(
|
||
&self,
|
||
cx: &App,
|
||
) -> (
|
||
String,
|
||
Vec<RowInfo>,
|
||
HashSet<MultiBufferRow>,
|
||
Vec<ReferenceRegion>,
|
||
) {
|
||
use util::maybe;
|
||
|
||
let mut text = String::new();
|
||
let mut regions = Vec::<ReferenceRegion>::new();
|
||
let mut excerpt_boundary_rows = HashSet::default();
|
||
for excerpt in &self.excerpts {
|
||
excerpt_boundary_rows.insert(MultiBufferRow(text.matches('\n').count() as u32));
|
||
let buffer = excerpt.buffer.read(cx);
|
||
let buffer_id = buffer.remote_id();
|
||
let buffer_range = excerpt.range.to_offset(buffer);
|
||
|
||
if let Some((diff, main_buffer)) = self.inverted_diffs.get(&buffer_id) {
|
||
let diff_snapshot = diff.read(cx).snapshot(cx);
|
||
let main_buffer_snapshot = main_buffer.read(cx).snapshot();
|
||
|
||
let mut offset = buffer_range.start;
|
||
for hunk in diff_snapshot.hunks_intersecting_base_text_range(
|
||
buffer_range.clone(),
|
||
&main_buffer_snapshot.text,
|
||
) {
|
||
let mut hunk_base_range = hunk.diff_base_byte_range.clone();
|
||
|
||
hunk_base_range.end = hunk_base_range.end.min(buffer_range.end);
|
||
if hunk_base_range.start > buffer_range.end
|
||
|| hunk_base_range.start < buffer_range.start
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// Add the text before the hunk
|
||
if hunk_base_range.start >= offset {
|
||
let len = text.len();
|
||
text.extend(buffer.text_for_range(offset..hunk_base_range.start));
|
||
if text.len() > len {
|
||
regions.push(ReferenceRegion {
|
||
buffer_id: Some(buffer_id),
|
||
range: len..text.len(),
|
||
buffer_range: (offset..hunk_base_range.start).to_point(&buffer),
|
||
status: None,
|
||
excerpt: Some(excerpt.clone()),
|
||
deleted_hunk_anchor: None,
|
||
});
|
||
}
|
||
}
|
||
|
||
// Add the "deleted" region (base text that's not in main)
|
||
if !hunk_base_range.is_empty() {
|
||
let len = text.len();
|
||
text.extend(buffer.text_for_range(hunk_base_range.clone()));
|
||
regions.push(ReferenceRegion {
|
||
buffer_id: Some(buffer_id),
|
||
range: len..text.len(),
|
||
buffer_range: hunk_base_range.to_point(&buffer),
|
||
status: Some(DiffHunkStatus::deleted(hunk.secondary_status)),
|
||
excerpt: Some(excerpt.clone()),
|
||
deleted_hunk_anchor: None,
|
||
});
|
||
}
|
||
|
||
offset = hunk_base_range.end;
|
||
}
|
||
|
||
// Add remaining buffer text
|
||
let len = text.len();
|
||
text.extend(buffer.text_for_range(offset..buffer_range.end));
|
||
text.push('\n');
|
||
regions.push(ReferenceRegion {
|
||
buffer_id: Some(buffer_id),
|
||
range: len..text.len(),
|
||
buffer_range: (offset..buffer_range.end).to_point(&buffer),
|
||
status: None,
|
||
excerpt: Some(excerpt.clone()),
|
||
deleted_hunk_anchor: None,
|
||
});
|
||
} else {
|
||
let diff = self.diffs.get(&buffer_id).unwrap().read(cx).snapshot(cx);
|
||
let base_buffer = diff.base_text();
|
||
|
||
let mut offset = buffer_range.start;
|
||
let hunks = diff
|
||
.hunks_intersecting_range(excerpt.range.clone(), buffer)
|
||
.peekable();
|
||
|
||
for hunk in hunks {
|
||
// Ignore hunks that are outside the excerpt range.
|
||
let mut hunk_range = hunk.buffer_range.to_offset(buffer);
|
||
|
||
hunk_range.end = hunk_range.end.min(buffer_range.end);
|
||
if hunk_range.start > buffer_range.end || hunk_range.start < buffer_range.start
|
||
{
|
||
log::trace!("skipping hunk outside excerpt range");
|
||
continue;
|
||
}
|
||
|
||
if !self
|
||
.expanded_diff_hunks_by_buffer
|
||
.get(&buffer_id)
|
||
.cloned()
|
||
.into_iter()
|
||
.flatten()
|
||
.any(|expanded_anchor| {
|
||
expanded_anchor
|
||
.cmp(&hunk.buffer_range.start, buffer)
|
||
.is_eq()
|
||
})
|
||
{
|
||
log::trace!("skipping a hunk that's not marked as expanded");
|
||
continue;
|
||
}
|
||
|
||
if !hunk.buffer_range.start.is_valid(buffer) {
|
||
log::trace!("skipping hunk with deleted start: {:?}", hunk.range);
|
||
continue;
|
||
}
|
||
|
||
if hunk_range.start >= offset {
|
||
// Add the buffer text before the hunk
|
||
let len = text.len();
|
||
text.extend(buffer.text_for_range(offset..hunk_range.start));
|
||
if text.len() > len {
|
||
regions.push(ReferenceRegion {
|
||
buffer_id: Some(buffer_id),
|
||
range: len..text.len(),
|
||
buffer_range: (offset..hunk_range.start).to_point(&buffer),
|
||
status: None,
|
||
excerpt: Some(excerpt.clone()),
|
||
deleted_hunk_anchor: None,
|
||
});
|
||
}
|
||
|
||
// Add the deleted text for the hunk.
|
||
if !hunk.diff_base_byte_range.is_empty() {
|
||
let mut base_text = base_buffer
|
||
.text_for_range(hunk.diff_base_byte_range.clone())
|
||
.collect::<String>();
|
||
if !base_text.ends_with('\n') {
|
||
base_text.push('\n');
|
||
}
|
||
let len = text.len();
|
||
text.push_str(&base_text);
|
||
regions.push(ReferenceRegion {
|
||
buffer_id: Some(base_buffer.remote_id()),
|
||
range: len..text.len(),
|
||
buffer_range: hunk.diff_base_byte_range.to_point(&base_buffer),
|
||
status: Some(DiffHunkStatus::deleted(hunk.secondary_status)),
|
||
excerpt: Some(excerpt.clone()),
|
||
deleted_hunk_anchor: Some(hunk.buffer_range.start),
|
||
});
|
||
}
|
||
|
||
offset = hunk_range.start;
|
||
}
|
||
|
||
// Add the inserted text for the hunk.
|
||
if hunk_range.end > offset {
|
||
let len = text.len();
|
||
text.extend(buffer.text_for_range(offset..hunk_range.end));
|
||
let range = len..text.len();
|
||
let region = ReferenceRegion {
|
||
buffer_id: Some(buffer_id),
|
||
range,
|
||
buffer_range: (offset..hunk_range.end).to_point(&buffer),
|
||
status: Some(DiffHunkStatus::added(hunk.secondary_status)),
|
||
excerpt: Some(excerpt.clone()),
|
||
deleted_hunk_anchor: None,
|
||
};
|
||
offset = hunk_range.end;
|
||
regions.push(region);
|
||
}
|
||
}
|
||
|
||
// Add the buffer text for the rest of the excerpt.
|
||
let len = text.len();
|
||
text.extend(buffer.text_for_range(offset..buffer_range.end));
|
||
text.push('\n');
|
||
regions.push(ReferenceRegion {
|
||
buffer_id: Some(buffer_id),
|
||
range: len..text.len(),
|
||
buffer_range: (offset..buffer_range.end).to_point(&buffer),
|
||
status: None,
|
||
excerpt: Some(excerpt.clone()),
|
||
deleted_hunk_anchor: None,
|
||
});
|
||
}
|
||
}
|
||
|
||
// Remove final trailing newline.
|
||
if self.excerpts.is_empty() {
|
||
regions.push(ReferenceRegion {
|
||
buffer_id: None,
|
||
range: 0..1,
|
||
buffer_range: Point::new(0, 0)..Point::new(0, 1),
|
||
status: None,
|
||
excerpt: None,
|
||
deleted_hunk_anchor: None,
|
||
});
|
||
} else {
|
||
text.pop();
|
||
let region = regions.last_mut().unwrap();
|
||
assert!(region.deleted_hunk_anchor.is_none());
|
||
region.range.end -= 1;
|
||
}
|
||
|
||
// Retrieve the row info using the region that contains
|
||
// the start of each multi-buffer line.
|
||
let mut ix = 0;
|
||
let row_infos = text
|
||
.split('\n')
|
||
.map(|line| {
|
||
let row_info = regions
|
||
.iter()
|
||
.rposition(|region| {
|
||
region.range.contains(&ix) || (ix == text.len() && ix == region.range.end)
|
||
})
|
||
.map_or(RowInfo::default(), |region_ix| {
|
||
let region = regions[region_ix].clone();
|
||
let buffer_row = region.buffer_range.start.row
|
||
+ text[region.range.start..ix].matches('\n').count() as u32;
|
||
let main_buffer = region.excerpt.as_ref().map(|e| e.buffer.clone());
|
||
let excerpt_range = region.excerpt.as_ref().map(|e| &e.range);
|
||
let is_excerpt_start = region_ix == 0
|
||
|| regions[region_ix - 1].excerpt.as_ref().map(|e| &e.range)
|
||
!= excerpt_range
|
||
|| regions[region_ix - 1].range.is_empty();
|
||
let mut is_excerpt_end = region_ix == regions.len() - 1
|
||
|| regions[region_ix + 1].excerpt.as_ref().map(|e| &e.range)
|
||
!= excerpt_range;
|
||
let is_start = !text[region.range.start..ix].contains('\n');
|
||
let is_last_region = region_ix == regions.len() - 1;
|
||
let mut is_end = if region.range.end > text.len() {
|
||
!text[ix..].contains('\n')
|
||
} else {
|
||
let remaining_newlines = text[ix..region.range.end.min(text.len())]
|
||
.matches('\n')
|
||
.count();
|
||
remaining_newlines == if is_last_region { 0 } else { 1 }
|
||
};
|
||
if region_ix < regions.len() - 1
|
||
&& !text[ix..].contains("\n")
|
||
&& (region.status == Some(DiffHunkStatus::added_none())
|
||
|| region.status.is_some_and(|s| s.is_deleted()))
|
||
&& regions[region_ix + 1].excerpt.as_ref().map(|e| &e.range)
|
||
== excerpt_range
|
||
&& regions[region_ix + 1].range.start == text.len()
|
||
{
|
||
is_end = true;
|
||
is_excerpt_end = true;
|
||
}
|
||
let multibuffer_row =
|
||
MultiBufferRow(text[..ix].matches('\n').count() as u32);
|
||
let mut expand_direction = None;
|
||
if let Some(buffer) = &main_buffer {
|
||
let needs_expand_up = is_excerpt_start && is_start && buffer_row > 0;
|
||
let needs_expand_down = is_excerpt_end
|
||
&& is_end
|
||
&& buffer.read(cx).max_point().row > buffer_row;
|
||
expand_direction = if needs_expand_up && needs_expand_down {
|
||
Some(ExpandExcerptDirection::UpAndDown)
|
||
} else if needs_expand_up {
|
||
Some(ExpandExcerptDirection::Up)
|
||
} else if needs_expand_down {
|
||
Some(ExpandExcerptDirection::Down)
|
||
} else {
|
||
None
|
||
};
|
||
}
|
||
RowInfo {
|
||
buffer_id: region.buffer_id,
|
||
diff_status: region.status,
|
||
buffer_row: Some(buffer_row),
|
||
wrapped_buffer_row: None,
|
||
|
||
multibuffer_row: Some(multibuffer_row),
|
||
expand_info: maybe!({
|
||
let direction = expand_direction?;
|
||
let excerpt = region.excerpt.as_ref()?;
|
||
Some(ExpandInfo {
|
||
direction,
|
||
start_anchor: Anchor::in_buffer(
|
||
excerpt.path_key_index,
|
||
excerpt.range.start,
|
||
),
|
||
})
|
||
}),
|
||
}
|
||
});
|
||
ix += line.len() + 1;
|
||
row_info
|
||
})
|
||
.collect();
|
||
|
||
(text, row_infos, excerpt_boundary_rows, regions)
|
||
}
|
||
|
||
fn diffs_updated(&mut self, cx: &mut App) {
|
||
let buffer_ids = self.diffs.keys().copied().collect::<Vec<_>>();
|
||
for buffer_id in buffer_ids {
|
||
self.update_expanded_diff_hunks_for_buffer(buffer_id, cx);
|
||
}
|
||
}
|
||
|
||
fn add_diff(&mut self, diff: Entity<BufferDiff>, cx: &mut App) {
|
||
let buffer_id = diff.read(cx).buffer_id;
|
||
self.diffs.insert(buffer_id, diff);
|
||
}
|
||
|
||
fn add_inverted_diff(
|
||
&mut self,
|
||
diff: Entity<BufferDiff>,
|
||
main_buffer: Entity<language::Buffer>,
|
||
cx: &App,
|
||
) {
|
||
let base_text_buffer_id = diff.read(cx).base_text(cx).remote_id();
|
||
self.inverted_diffs
|
||
.insert(base_text_buffer_id, (diff, main_buffer));
|
||
}
|
||
|
||
fn update_expanded_diff_hunks_for_buffer(&mut self, buffer_id: BufferId, cx: &mut App) {
|
||
let excerpts = self
|
||
.excerpts
|
||
.iter()
|
||
.filter(|excerpt| excerpt.buffer.read(cx).remote_id() == buffer_id)
|
||
.collect::<Vec<_>>();
|
||
let Some(buffer) = excerpts.first().map(|excerpt| excerpt.buffer.clone()) else {
|
||
self.expanded_diff_hunks_by_buffer.remove(&buffer_id);
|
||
return;
|
||
};
|
||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||
let Some(diff) = self.diffs.get(&buffer_id) else {
|
||
self.expanded_diff_hunks_by_buffer.remove(&buffer_id);
|
||
return;
|
||
};
|
||
let diff = diff.read(cx).snapshot(cx);
|
||
let hunks = diff
|
||
.hunks_in_row_range(0..u32::MAX, &buffer_snapshot)
|
||
.collect::<Vec<_>>();
|
||
self.expanded_diff_hunks_by_buffer
|
||
.entry(buffer_id)
|
||
.or_default()
|
||
.retain(|hunk_anchor| {
|
||
if !hunk_anchor.is_valid(&buffer_snapshot) {
|
||
return false;
|
||
}
|
||
|
||
let Ok(ix) = hunks.binary_search_by(|hunk| {
|
||
hunk.buffer_range.start.cmp(hunk_anchor, &buffer_snapshot)
|
||
}) else {
|
||
return false;
|
||
};
|
||
let hunk_range = hunks[ix].buffer_range.to_point(&buffer_snapshot);
|
||
excerpts.iter().any(|excerpt| {
|
||
let excerpt_range = excerpt.range.to_point(&buffer_snapshot);
|
||
hunk_range.start >= excerpt_range.start && hunk_range.start <= excerpt_range.end
|
||
})
|
||
});
|
||
}
|
||
|
||
fn anchor_to_offset(&self, anchor: &Anchor, cx: &App) -> Option<MultiBufferOffset> {
|
||
if anchor.diff_base_anchor().is_some() {
|
||
panic!("reference multibuffer cannot yet resolve anchors inside deleted hunks");
|
||
}
|
||
let (anchor, snapshot, path_key) = self.anchor_to_buffer_anchor(anchor, cx)?;
|
||
// TODO(cole) can maybe make this and expected content call a common function instead
|
||
let (text, _, _, regions) = self.expected_content(cx);
|
||
|
||
// Locate the first region that contains or is past the putative location of the buffer anchor
|
||
let ix = regions.partition_point(|region| {
|
||
let excerpt = region
|
||
.excerpt
|
||
.as_ref()
|
||
.expect("should have no buffers in empty reference multibuffer");
|
||
excerpt
|
||
.path_key
|
||
.cmp(&path_key)
|
||
.then_with(|| {
|
||
if excerpt.range.end.cmp(&anchor, &snapshot).is_lt() {
|
||
Ordering::Less
|
||
} else if excerpt.range.start.cmp(&anchor, &snapshot).is_gt() {
|
||
Ordering::Greater
|
||
} else {
|
||
Ordering::Equal
|
||
}
|
||
})
|
||
.then_with(|| {
|
||
if let Some(deleted_hunk_anchor) = region.deleted_hunk_anchor {
|
||
deleted_hunk_anchor.cmp(&anchor, &snapshot)
|
||
} else {
|
||
let point = anchor.to_point(&snapshot);
|
||
assert_eq!(region.buffer_id, Some(snapshot.remote_id()));
|
||
if region.buffer_range.end < point {
|
||
Ordering::Less
|
||
} else if region.buffer_range.start > point {
|
||
Ordering::Greater
|
||
} else {
|
||
Ordering::Equal
|
||
}
|
||
}
|
||
})
|
||
.is_lt()
|
||
});
|
||
|
||
let Some(region) = regions.get(ix) else {
|
||
return Some(MultiBufferOffset(text.len()));
|
||
};
|
||
|
||
let offset = if region.buffer_id == Some(snapshot.remote_id()) {
|
||
let buffer_offset = anchor.to_offset(&snapshot);
|
||
let buffer_range = region.buffer_range.to_offset(&snapshot);
|
||
assert!(buffer_offset <= buffer_range.end);
|
||
let overshoot = buffer_offset.saturating_sub(buffer_range.start);
|
||
region.range.start + overshoot
|
||
} else {
|
||
region.range.start
|
||
};
|
||
Some(MultiBufferOffset(offset))
|
||
}
|
||
|
||
fn anchor_to_buffer_anchor(
|
||
&self,
|
||
anchor: &Anchor,
|
||
cx: &App,
|
||
) -> Option<(text::Anchor, BufferSnapshot, PathKey)> {
|
||
let (excerpt, anchor) = match anchor {
|
||
Anchor::Min => {
|
||
let excerpt = self.excerpts.first()?;
|
||
(excerpt, excerpt.range.start)
|
||
}
|
||
Anchor::Excerpt(excerpt_anchor) => (
|
||
self.excerpts.iter().find(|excerpt| {
|
||
excerpt.buffer.read(cx).remote_id() == excerpt_anchor.buffer_id()
|
||
})?,
|
||
excerpt_anchor.text_anchor,
|
||
),
|
||
Anchor::Max => {
|
||
let excerpt = self.excerpts.last()?;
|
||
(excerpt, excerpt.range.end)
|
||
}
|
||
};
|
||
|
||
Some((
|
||
anchor,
|
||
excerpt.buffer.read(cx).snapshot(),
|
||
excerpt.path_key.clone(),
|
||
))
|
||
}
|
||
}
|
||
|
||
#[gpui::test(iterations = 100)]
|
||
async fn test_random_set_ranges(cx: &mut TestAppContext, mut rng: StdRng) {
|
||
let base_text = "a\n".repeat(100);
|
||
let buf = cx.update(|cx| cx.new(|cx| Buffer::local(base_text, cx)));
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
|
||
let operations = env::var("OPERATIONS")
|
||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||
.unwrap_or(10);
|
||
|
||
fn row_ranges(ranges: &Vec<Range<Point>>) -> Vec<Range<u32>> {
|
||
ranges
|
||
.iter()
|
||
.map(|range| range.start.row..range.end.row)
|
||
.collect()
|
||
}
|
||
|
||
for _ in 0..operations {
|
||
let snapshot = buf.update(cx, |buf, _| buf.snapshot());
|
||
let num_ranges = rng.random_range(0..=10);
|
||
let max_row = snapshot.max_point().row;
|
||
let mut ranges = (0..num_ranges)
|
||
.map(|_| {
|
||
let start = rng.random_range(0..max_row);
|
||
let end = rng.random_range(start + 1..max_row + 1);
|
||
Point::row_range(start..end)
|
||
})
|
||
.collect::<Vec<_>>();
|
||
ranges.sort_by_key(|range| range.start);
|
||
log::info!("Setting ranges: {:?}", row_ranges(&ranges));
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::for_buffer(&buf, cx),
|
||
buf.clone(),
|
||
ranges.clone(),
|
||
2,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||
let mut last_end = None;
|
||
let mut seen_ranges = Vec::default();
|
||
|
||
for info in snapshot.excerpts() {
|
||
let buffer_snapshot = snapshot
|
||
.buffer_for_id(info.context.start.buffer_id)
|
||
.unwrap();
|
||
let start = info.context.start.to_point(buffer_snapshot);
|
||
let end = info.context.end.to_point(buffer_snapshot);
|
||
seen_ranges.push(start..end);
|
||
|
||
if let Some(last_end) = last_end.take() {
|
||
assert!(
|
||
start > last_end,
|
||
"multibuffer has out-of-order ranges: {:?}; {:?} <= {:?}",
|
||
row_ranges(&seen_ranges),
|
||
start,
|
||
last_end
|
||
)
|
||
}
|
||
|
||
ranges.retain(|range| range.start < start || range.end > end);
|
||
|
||
last_end = Some(end)
|
||
}
|
||
|
||
assert!(
|
||
ranges.is_empty(),
|
||
"multibuffer {:?} did not include all ranges: {:?}",
|
||
row_ranges(&seen_ranges),
|
||
row_ranges(&ranges)
|
||
);
|
||
}
|
||
}
|
||
|
||
#[gpui::test(iterations = 100)]
|
||
async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
|
||
let operations = env::var("OPERATIONS")
|
||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||
.unwrap_or(10);
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
let mut buffers: Vec<Entity<Buffer>> = Vec::new();
|
||
let mut base_texts: HashMap<BufferId, String> = HashMap::default();
|
||
let mut reference = ReferenceMultibuffer::default();
|
||
let mut anchors = Vec::new();
|
||
let mut old_versions = Vec::new();
|
||
let mut needs_diff_calculation = false;
|
||
let mut inverted_diff_main_buffers: HashMap<BufferId, Entity<BufferDiff>> = HashMap::default();
|
||
for _ in 0..operations {
|
||
match rng.random_range(0..100) {
|
||
0..=14 if !buffers.is_empty() => {
|
||
let buffer = buffers.choose(&mut rng).unwrap();
|
||
buffer.update(cx, |buf, cx| {
|
||
let edit_count = rng.random_range(1..5);
|
||
buf.randomly_edit(&mut rng, edit_count, cx);
|
||
log::info!("buffer text:\n{}", buf.text());
|
||
needs_diff_calculation = true;
|
||
});
|
||
cx.update(|cx| reference.diffs_updated(cx));
|
||
}
|
||
15..=24 if !reference.excerpts.is_empty() => {
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
let infos = snapshot.excerpts().collect::<Vec<_>>();
|
||
let mut excerpts = HashSet::default();
|
||
for _ in 0..rng.random_range(0..infos.len()) {
|
||
excerpts.extend(infos.choose(&mut rng).cloned());
|
||
}
|
||
|
||
let line_count = rng.random_range(0..5);
|
||
|
||
let excerpt_ixs = excerpts
|
||
.iter()
|
||
.map(|info| {
|
||
reference
|
||
.excerpts
|
||
.iter()
|
||
.position(|e| e.range == info.context)
|
||
.unwrap()
|
||
})
|
||
.collect::<Vec<_>>();
|
||
log::info!("Expanding excerpts {excerpt_ixs:?} by {line_count} lines");
|
||
multibuffer.expand_excerpts(
|
||
excerpts
|
||
.iter()
|
||
.map(|info| snapshot.anchor_in_excerpt(info.context.end).unwrap()),
|
||
line_count,
|
||
ExpandExcerptDirection::UpAndDown,
|
||
cx,
|
||
);
|
||
|
||
reference.expand_excerpts(&excerpts, line_count, cx);
|
||
});
|
||
}
|
||
25..=34 if !reference.excerpts.is_empty() => {
|
||
let multibuffer =
|
||
multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||
let offset = multibuffer.clip_offset(
|
||
MultiBufferOffset(rng.random_range(0..=multibuffer.len().0)),
|
||
Bias::Left,
|
||
);
|
||
let bias = if rng.random() {
|
||
Bias::Left
|
||
} else {
|
||
Bias::Right
|
||
};
|
||
log::info!("Creating anchor at {} with bias {:?}", offset.0, bias);
|
||
anchors.push(multibuffer.anchor_at(offset, bias));
|
||
anchors.sort_by(|a, b| a.cmp(b, &multibuffer));
|
||
}
|
||
35..=45 if !reference.excerpts.is_empty() => {
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
let excerpt_ix = rng.random_range(0..reference.excerpts.len());
|
||
let excerpt = &reference.excerpts[excerpt_ix];
|
||
|
||
// Skip inverted excerpts - hunks can't be collapsed
|
||
let buffer_id = excerpt.buffer.read(cx).remote_id();
|
||
if reference.inverted_diffs.contains_key(&buffer_id) {
|
||
return;
|
||
}
|
||
|
||
let start = excerpt.range.start;
|
||
let end = excerpt.range.end;
|
||
let range = snapshot.anchor_in_excerpt(start).unwrap()
|
||
..snapshot.anchor_in_excerpt(end).unwrap();
|
||
|
||
log::info!(
|
||
"expanding diff hunks in range {:?} (excerpt index {excerpt_ix:?}, buffer id {:?})",
|
||
range.to_point(&snapshot),
|
||
buffer_id,
|
||
);
|
||
reference.expand_diff_hunks(excerpt.path_key.clone(), start..end, cx);
|
||
multibuffer.expand_diff_hunks(vec![range], cx);
|
||
});
|
||
}
|
||
46..=75 if needs_diff_calculation => {
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
for buffer in multibuffer.all_buffers() {
|
||
let snapshot = buffer.read(cx).snapshot();
|
||
let buffer_id = snapshot.remote_id();
|
||
|
||
if let Some(diff) = multibuffer.diff_for(buffer_id) {
|
||
diff.update(cx, |diff, cx| {
|
||
log::info!("recalculating diff for buffer {:?}", buffer_id,);
|
||
diff.recalculate_diff_sync(&snapshot.text, cx);
|
||
});
|
||
}
|
||
|
||
if let Some(inverted_diff) = inverted_diff_main_buffers.get(&buffer_id) {
|
||
inverted_diff.update(cx, |diff, cx| {
|
||
log::info!(
|
||
"recalculating inverted diff for main buffer {:?}",
|
||
buffer_id,
|
||
);
|
||
diff.recalculate_diff_sync(&snapshot.text, cx);
|
||
});
|
||
}
|
||
}
|
||
reference.diffs_updated(cx);
|
||
needs_diff_calculation = false;
|
||
});
|
||
}
|
||
_ => {
|
||
// Decide if we're creating a new buffer or reusing an existing one
|
||
let create_new_buffer = buffers.is_empty() || rng.random_bool(0.4);
|
||
|
||
let (excerpt_buffer, diff, inverted_main_buffer) = if create_new_buffer {
|
||
let create_inverted = rng.random_bool(0.3);
|
||
|
||
if create_inverted {
|
||
let mut main_buffer_text = util::RandomCharIter::new(&mut rng)
|
||
.take(256)
|
||
.collect::<String>();
|
||
let main_buffer = cx.new(|cx| Buffer::local(main_buffer_text.clone(), cx));
|
||
text::LineEnding::normalize(&mut main_buffer_text);
|
||
let main_buffer_id =
|
||
main_buffer.read_with(cx, |buffer, _| buffer.remote_id());
|
||
base_texts.insert(main_buffer_id, main_buffer_text.clone());
|
||
buffers.push(main_buffer.clone());
|
||
|
||
let diff = cx.new(|cx| {
|
||
BufferDiff::new_with_base_text(
|
||
&main_buffer_text,
|
||
&main_buffer.read(cx).text_snapshot(),
|
||
cx,
|
||
)
|
||
});
|
||
|
||
let base_text_buffer =
|
||
diff.read_with(cx, |diff, _| diff.base_text_buffer().clone());
|
||
|
||
// Track for recalculation when main buffer is edited
|
||
inverted_diff_main_buffers.insert(main_buffer_id, diff.clone());
|
||
|
||
(base_text_buffer, diff, Some(main_buffer))
|
||
} else {
|
||
let mut base_text = util::RandomCharIter::new(&mut rng)
|
||
.take(256)
|
||
.collect::<String>();
|
||
|
||
let buffer_handle = cx.new(|cx| Buffer::local(base_text.clone(), cx));
|
||
text::LineEnding::normalize(&mut base_text);
|
||
let buffer_id = buffer_handle.read_with(cx, |buffer, _| buffer.remote_id());
|
||
base_texts.insert(buffer_id, base_text.clone());
|
||
buffers.push(buffer_handle.clone());
|
||
|
||
let diff = cx.new(|cx| {
|
||
BufferDiff::new_with_base_text(
|
||
&base_text,
|
||
&buffer_handle.read(cx).text_snapshot(),
|
||
cx,
|
||
)
|
||
});
|
||
|
||
(buffer_handle, diff, None)
|
||
}
|
||
} else {
|
||
// Reuse an existing buffer
|
||
let buffer_handle = buffers.choose(&mut rng).unwrap().clone();
|
||
let buffer_id = buffer_handle.read_with(cx, |buffer, _| buffer.remote_id());
|
||
|
||
if let Some(diff) = inverted_diff_main_buffers.get(&buffer_id) {
|
||
let base_text_buffer =
|
||
diff.read_with(cx, |diff, _| diff.base_text_buffer().clone());
|
||
(base_text_buffer, diff.clone(), Some(buffer_handle))
|
||
} else {
|
||
// Get existing diff or create new one for regular buffer
|
||
let diff = multibuffer
|
||
.read_with(cx, |mb, _| mb.diff_for(buffer_id))
|
||
.unwrap_or_else(|| {
|
||
let base_text = base_texts.get(&buffer_id).unwrap();
|
||
cx.new(|cx| {
|
||
BufferDiff::new_with_base_text(
|
||
base_text,
|
||
&buffer_handle.read(cx).text_snapshot(),
|
||
cx,
|
||
)
|
||
})
|
||
});
|
||
(buffer_handle, diff, None)
|
||
}
|
||
};
|
||
|
||
let excerpt_buffer_snapshot =
|
||
excerpt_buffer.read_with(cx, |excerpt_buffer, _| excerpt_buffer.snapshot());
|
||
let mut ranges = reference
|
||
.excerpts
|
||
.iter()
|
||
.filter(|excerpt| excerpt.buffer == excerpt_buffer)
|
||
.map(|excerpt| excerpt.range.to_point(&excerpt_buffer_snapshot))
|
||
.collect::<Vec<_>>();
|
||
mutate_excerpt_ranges(&mut rng, &mut ranges, &excerpt_buffer_snapshot, 1);
|
||
let ranges = ranges
|
||
.iter()
|
||
.cloned()
|
||
.map(ExcerptRange::new)
|
||
.collect::<Vec<_>>();
|
||
let path = cx.update(|cx| PathKey::for_buffer(&excerpt_buffer, cx));
|
||
let path_key_index = multibuffer.update(cx, |multibuffer, _| {
|
||
multibuffer.get_or_create_path_key_index(&path)
|
||
});
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpt_ranges_for_path(
|
||
path.clone(),
|
||
excerpt_buffer.clone(),
|
||
&excerpt_buffer_snapshot,
|
||
ranges.clone(),
|
||
cx,
|
||
)
|
||
});
|
||
|
||
cx.update(|cx| {
|
||
reference.set_excerpts(
|
||
path,
|
||
path_key_index,
|
||
excerpt_buffer.clone(),
|
||
&excerpt_buffer_snapshot,
|
||
ranges,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
let excerpt_buffer_id =
|
||
excerpt_buffer.read_with(cx, |buffer, _| buffer.remote_id());
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
if multibuffer.diff_for(excerpt_buffer_id).is_none() {
|
||
if let Some(main_buffer) = inverted_main_buffer {
|
||
reference.add_inverted_diff(diff.clone(), main_buffer.clone(), cx);
|
||
multibuffer.add_inverted_diff(diff, main_buffer, cx);
|
||
} else {
|
||
reference.add_diff(diff.clone(), cx);
|
||
multibuffer.add_diff(diff, cx);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
if rng.random_bool(0.3) {
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe()));
|
||
})
|
||
}
|
||
|
||
multibuffer.read_with(cx, |multibuffer, cx| {
|
||
check_multibuffer(multibuffer, &reference, &anchors, cx, &mut rng);
|
||
});
|
||
}
|
||
let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||
for (old_snapshot, subscription) in old_versions {
|
||
check_multibuffer_edits(&snapshot, &old_snapshot, subscription);
|
||
}
|
||
}
|
||
|
||
fn mutate_excerpt_ranges(
|
||
rng: &mut StdRng,
|
||
existing_ranges: &mut Vec<Range<Point>>,
|
||
buffer: &BufferSnapshot,
|
||
operations: u32,
|
||
) {
|
||
let mut ranges_to_add = Vec::new();
|
||
|
||
for _ in 0..operations {
|
||
match rng.random_range(0..5) {
|
||
0..=1 if !existing_ranges.is_empty() => {
|
||
let index = rng.random_range(0..existing_ranges.len());
|
||
log::info!("Removing excerpt at index {index}");
|
||
existing_ranges.remove(index);
|
||
}
|
||
_ => {
|
||
let end_row = rng.random_range(0..=buffer.max_point().row);
|
||
let start_row = rng.random_range(0..=end_row);
|
||
let end_col = buffer.line_len(end_row);
|
||
log::info!(
|
||
"Inserting excerpt for buffer {:?}, row range {:?}",
|
||
buffer.remote_id(),
|
||
start_row..end_row
|
||
);
|
||
ranges_to_add.push(Point::new(start_row, 0)..Point::new(end_row, end_col));
|
||
}
|
||
}
|
||
}
|
||
|
||
existing_ranges.extend(ranges_to_add);
|
||
existing_ranges.sort_by_key(|r| r.start);
|
||
}
|
||
|
||
fn check_multibuffer(
|
||
multibuffer: &MultiBuffer,
|
||
reference: &ReferenceMultibuffer,
|
||
anchors: &[Anchor],
|
||
cx: &App,
|
||
rng: &mut StdRng,
|
||
) {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
let actual_text = snapshot.text();
|
||
let actual_boundary_rows = snapshot
|
||
.excerpt_boundaries_in_range(MultiBufferOffset(0)..)
|
||
.map(|b| b.row)
|
||
.collect::<HashSet<_>>();
|
||
let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
|
||
|
||
let anchors_to_check = anchors
|
||
.iter()
|
||
.filter_map(|anchor| {
|
||
snapshot
|
||
.anchor_to_buffer_anchor(*anchor)
|
||
.map(|(anchor, _)| anchor)
|
||
})
|
||
// Intentionally mix in some anchors that are (in general) not contained in any excerpt
|
||
.chain(
|
||
reference
|
||
.excerpts
|
||
.iter()
|
||
.map(|excerpt| excerpt.buffer.read(cx).remote_id())
|
||
.dedup()
|
||
.flat_map(|buffer_id| {
|
||
[
|
||
text::Anchor::min_for_buffer(buffer_id),
|
||
text::Anchor::max_for_buffer(buffer_id),
|
||
]
|
||
}),
|
||
)
|
||
.map(|anchor| snapshot.anchor_in_buffer(anchor).unwrap())
|
||
.collect::<Vec<_>>();
|
||
|
||
let (expected_text, expected_row_infos, expected_boundary_rows, _) =
|
||
reference.expected_content(cx);
|
||
let expected_anchor_offsets = anchors_to_check
|
||
.iter()
|
||
.map(|anchor| reference.anchor_to_offset(anchor, cx).unwrap())
|
||
.collect::<Vec<_>>();
|
||
|
||
let has_diff = actual_row_infos
|
||
.iter()
|
||
.any(|info| info.diff_status.is_some())
|
||
|| expected_row_infos
|
||
.iter()
|
||
.any(|info| info.diff_status.is_some());
|
||
let actual_diff = format_diff(
|
||
&actual_text,
|
||
&actual_row_infos,
|
||
&actual_boundary_rows,
|
||
Some(has_diff),
|
||
);
|
||
let expected_diff = format_diff(
|
||
&expected_text,
|
||
&expected_row_infos,
|
||
&expected_boundary_rows,
|
||
Some(has_diff),
|
||
);
|
||
|
||
log::info!("Multibuffer content:\n{}", actual_diff);
|
||
|
||
assert_eq!(
|
||
actual_row_infos.len(),
|
||
actual_text.split('\n').count(),
|
||
"line count: {}",
|
||
actual_text.split('\n').count()
|
||
);
|
||
pretty_assertions::assert_eq!(actual_diff, expected_diff);
|
||
pretty_assertions::assert_eq!(actual_text, expected_text);
|
||
pretty_assertions::assert_eq!(actual_row_infos, expected_row_infos);
|
||
|
||
for _ in 0..5 {
|
||
let start_row = rng.random_range(0..=expected_row_infos.len());
|
||
assert_eq!(
|
||
snapshot
|
||
.row_infos(MultiBufferRow(start_row as u32))
|
||
.collect::<Vec<_>>(),
|
||
&expected_row_infos[start_row..],
|
||
"buffer_rows({})",
|
||
start_row
|
||
);
|
||
}
|
||
|
||
assert_eq!(
|
||
snapshot.widest_line_number(),
|
||
expected_row_infos
|
||
.into_iter()
|
||
.filter_map(|info| {
|
||
// For inverted diffs, deleted rows are visible and should be counted.
|
||
// Only filter out deleted rows that are NOT from inverted diffs.
|
||
let is_inverted_diff = info
|
||
.buffer_id
|
||
.is_some_and(|id| reference.inverted_diffs.contains_key(&id));
|
||
if info.diff_status.is_some_and(|status| status.is_deleted()) && !is_inverted_diff {
|
||
None
|
||
} else {
|
||
info.buffer_row
|
||
}
|
||
})
|
||
.max()
|
||
.unwrap()
|
||
+ 1
|
||
);
|
||
for i in 0..snapshot.len().0 {
|
||
let (_, excerpt_range) = snapshot
|
||
.excerpt_containing(MultiBufferOffset(i)..MultiBufferOffset(i))
|
||
.unwrap();
|
||
reference
|
||
.excerpts
|
||
.iter()
|
||
.find(|reference_excerpt| reference_excerpt.range == excerpt_range.context)
|
||
.expect("corresponding excerpt should exist in reference multibuffer");
|
||
}
|
||
|
||
assert_consistent_line_numbers(&snapshot);
|
||
assert_position_translation(&snapshot);
|
||
|
||
for (row, line) in expected_text.split('\n').enumerate() {
|
||
assert_eq!(
|
||
snapshot.line_len(MultiBufferRow(row as u32)),
|
||
line.len() as u32,
|
||
"line_len({}).",
|
||
row
|
||
);
|
||
}
|
||
|
||
let text_rope = Rope::from(expected_text.as_str());
|
||
for _ in 0..10 {
|
||
let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right);
|
||
let start_ix = text_rope.clip_offset(rng.random_range(0..=end_ix), Bias::Left);
|
||
|
||
let text_for_range = snapshot
|
||
.text_for_range(MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix))
|
||
.collect::<String>();
|
||
assert_eq!(
|
||
text_for_range,
|
||
&expected_text[start_ix..end_ix],
|
||
"incorrect text for range {:?}",
|
||
start_ix..end_ix
|
||
);
|
||
|
||
let expected_summary =
|
||
MBTextSummary::from(TextSummary::from(&expected_text[start_ix..end_ix]));
|
||
assert_eq!(
|
||
snapshot.text_summary_for_range::<MBTextSummary, _>(
|
||
MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix)
|
||
),
|
||
expected_summary,
|
||
"incorrect summary for range {:?}",
|
||
start_ix..end_ix
|
||
);
|
||
}
|
||
|
||
// Anchor resolution
|
||
let summaries = snapshot.summaries_for_anchors::<MultiBufferOffset, _>(anchors);
|
||
assert_eq!(anchors.len(), summaries.len());
|
||
for (anchor, resolved_offset) in anchors.iter().zip(summaries) {
|
||
assert!(resolved_offset <= snapshot.len());
|
||
assert_eq!(
|
||
snapshot.summary_for_anchor::<MultiBufferOffset>(anchor),
|
||
resolved_offset,
|
||
"anchor: {:?}",
|
||
anchor
|
||
);
|
||
}
|
||
|
||
let actual_anchor_offsets = anchors_to_check
|
||
.into_iter()
|
||
.map(|anchor| anchor.to_offset(&snapshot))
|
||
.collect::<Vec<_>>();
|
||
assert_eq!(
|
||
actual_anchor_offsets, expected_anchor_offsets,
|
||
"buffer anchor resolves to wrong offset"
|
||
);
|
||
|
||
for _ in 0..10 {
|
||
let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right);
|
||
assert_eq!(
|
||
snapshot
|
||
.reversed_chars_at(MultiBufferOffset(end_ix))
|
||
.collect::<String>(),
|
||
expected_text[..end_ix].chars().rev().collect::<String>(),
|
||
);
|
||
}
|
||
|
||
for _ in 0..10 {
|
||
let end_ix = rng.random_range(0..=text_rope.len());
|
||
let end_ix = text_rope.floor_char_boundary(end_ix);
|
||
let start_ix = rng.random_range(0..=end_ix);
|
||
let start_ix = text_rope.floor_char_boundary(start_ix);
|
||
assert_eq!(
|
||
snapshot
|
||
.bytes_in_range(MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix))
|
||
.flatten()
|
||
.copied()
|
||
.collect::<Vec<_>>(),
|
||
expected_text.as_bytes()[start_ix..end_ix].to_vec(),
|
||
"bytes_in_range({:?})",
|
||
start_ix..end_ix,
|
||
);
|
||
}
|
||
}
|
||
|
||
fn check_multibuffer_edits(
|
||
snapshot: &MultiBufferSnapshot,
|
||
old_snapshot: &MultiBufferSnapshot,
|
||
subscription: Subscription<MultiBufferOffset>,
|
||
) {
|
||
let edits = subscription.consume().into_inner();
|
||
|
||
log::info!(
|
||
"applying subscription edits to old text: {:?}: {:#?}",
|
||
old_snapshot.text(),
|
||
edits,
|
||
);
|
||
|
||
let mut text = old_snapshot.text();
|
||
for edit in edits {
|
||
let new_text: String = snapshot
|
||
.text_for_range(edit.new.start..edit.new.end)
|
||
.collect();
|
||
text.replace_range(
|
||
(edit.new.start.0..edit.new.start.0 + (edit.old.end.0 - edit.old.start.0)).clone(),
|
||
&new_text,
|
||
);
|
||
pretty_assertions::assert_eq!(
|
||
&text[0..edit.new.end.0],
|
||
snapshot
|
||
.text_for_range(MultiBufferOffset(0)..edit.new.end)
|
||
.collect::<String>()
|
||
);
|
||
}
|
||
pretty_assertions::assert_eq!(text, snapshot.text());
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_history(cx: &mut App) {
|
||
let test_settings = SettingsStore::test(cx);
|
||
cx.set_global(test_settings);
|
||
|
||
let group_interval: Duration = Duration::from_millis(1);
|
||
let buffer_1 = cx.new(|cx| {
|
||
let mut buf = Buffer::local("1234", cx);
|
||
buf.set_group_interval(group_interval);
|
||
buf
|
||
});
|
||
let buffer_2 = cx.new(|cx| {
|
||
let mut buf = Buffer::local("5678", cx);
|
||
buf.set_group_interval(group_interval);
|
||
buf
|
||
});
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
multibuffer.update(cx, |this, cx| {
|
||
this.set_group_interval(group_interval, cx);
|
||
});
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(0),
|
||
buffer_1.clone(),
|
||
[Point::zero()..buffer_1.read(cx).max_point()],
|
||
0,
|
||
cx,
|
||
);
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(1),
|
||
buffer_2.clone(),
|
||
[Point::zero()..buffer_2.read(cx).max_point()],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
let mut now = Instant::now();
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
let transaction_1 = multibuffer.start_transaction_at(now, cx).unwrap();
|
||
multibuffer.edit(
|
||
[
|
||
(Point::new(0, 0)..Point::new(0, 0), "A"),
|
||
(Point::new(1, 0)..Point::new(1, 0), "A"),
|
||
],
|
||
None,
|
||
cx,
|
||
);
|
||
multibuffer.edit(
|
||
[
|
||
(Point::new(0, 1)..Point::new(0, 1), "B"),
|
||
(Point::new(1, 1)..Point::new(1, 1), "B"),
|
||
],
|
||
None,
|
||
cx,
|
||
);
|
||
multibuffer.end_transaction_at(now, cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");
|
||
|
||
// Verify edited ranges for transaction 1
|
||
assert_eq!(
|
||
multibuffer.edited_ranges_for_transaction(transaction_1, cx),
|
||
&[
|
||
MultiBufferOffset(0)..MultiBufferOffset(2),
|
||
MultiBufferOffset(7)..MultiBufferOffset(9),
|
||
]
|
||
);
|
||
|
||
// Edit buffer 1 through the multibuffer
|
||
now += 2 * group_interval;
|
||
multibuffer.start_transaction_at(now, cx);
|
||
multibuffer.edit(
|
||
[(MultiBufferOffset(2)..MultiBufferOffset(2), "C")],
|
||
None,
|
||
cx,
|
||
);
|
||
multibuffer.end_transaction_at(now, cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678");
|
||
|
||
// Edit buffer 1 independently
|
||
buffer_1.update(cx, |buffer_1, cx| {
|
||
buffer_1.start_transaction_at(now);
|
||
buffer_1.edit([(3..3, "D")], None, cx);
|
||
buffer_1.end_transaction_at(now, cx);
|
||
|
||
now += 2 * group_interval;
|
||
buffer_1.start_transaction_at(now);
|
||
buffer_1.edit([(4..4, "E")], None, cx);
|
||
buffer_1.end_transaction_at(now, cx);
|
||
});
|
||
assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678");
|
||
|
||
// An undo in the multibuffer undoes the multibuffer transaction
|
||
// and also any individual buffer edits that have occurred since
|
||
// that transaction.
|
||
multibuffer.undo(cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");
|
||
|
||
multibuffer.undo(cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "1234\n5678");
|
||
|
||
multibuffer.redo(cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");
|
||
|
||
multibuffer.redo(cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678");
|
||
|
||
// Undo buffer 2 independently.
|
||
buffer_2.update(cx, |buffer_2, cx| buffer_2.undo(cx));
|
||
assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\n5678");
|
||
|
||
// An undo in the multibuffer undoes the components of the
|
||
// the last multibuffer transaction that are not already undone.
|
||
multibuffer.undo(cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "AB1234\n5678");
|
||
|
||
multibuffer.undo(cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "1234\n5678");
|
||
|
||
multibuffer.redo(cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");
|
||
|
||
buffer_1.update(cx, |buffer_1, cx| buffer_1.redo(cx));
|
||
assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678");
|
||
|
||
// Redo stack gets cleared after an edit.
|
||
now += 2 * group_interval;
|
||
multibuffer.start_transaction_at(now, cx);
|
||
multibuffer.edit(
|
||
[(MultiBufferOffset(0)..MultiBufferOffset(0), "X")],
|
||
None,
|
||
cx,
|
||
);
|
||
multibuffer.end_transaction_at(now, cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
|
||
multibuffer.redo(cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
|
||
multibuffer.undo(cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678");
|
||
multibuffer.undo(cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "1234\n5678");
|
||
|
||
// Transactions can be grouped manually.
|
||
multibuffer.redo(cx);
|
||
multibuffer.redo(cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
|
||
multibuffer.group_until_transaction(transaction_1, cx);
|
||
multibuffer.undo(cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "1234\n5678");
|
||
multibuffer.redo(cx);
|
||
assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_enclosing_indent(cx: &mut TestAppContext) {
|
||
async fn enclosing_indent(
|
||
text: &str,
|
||
buffer_row: u32,
|
||
cx: &mut TestAppContext,
|
||
) -> Option<(Range<u32>, LineIndent)> {
|
||
let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
|
||
let snapshot = cx.read(|cx| buffer.read(cx).snapshot(cx));
|
||
let (range, indent) = snapshot
|
||
.enclosing_indent(MultiBufferRow(buffer_row))
|
||
.await?;
|
||
Some((range.start.0..range.end.0, indent))
|
||
}
|
||
|
||
assert_eq!(
|
||
enclosing_indent(
|
||
indoc!(
|
||
"
|
||
fn b() {
|
||
if c {
|
||
let d = 2;
|
||
}
|
||
}
|
||
"
|
||
),
|
||
1,
|
||
cx,
|
||
)
|
||
.await,
|
||
Some((
|
||
1..2,
|
||
LineIndent {
|
||
tabs: 0,
|
||
spaces: 4,
|
||
line_blank: false,
|
||
}
|
||
))
|
||
);
|
||
|
||
assert_eq!(
|
||
enclosing_indent(
|
||
indoc!(
|
||
"
|
||
fn b() {
|
||
if c {
|
||
let d = 2;
|
||
}
|
||
}
|
||
"
|
||
),
|
||
2,
|
||
cx,
|
||
)
|
||
.await,
|
||
Some((
|
||
1..2,
|
||
LineIndent {
|
||
tabs: 0,
|
||
spaces: 4,
|
||
line_blank: false,
|
||
}
|
||
))
|
||
);
|
||
|
||
assert_eq!(
|
||
enclosing_indent(
|
||
indoc!(
|
||
"
|
||
fn b() {
|
||
if c {
|
||
let d = 2;
|
||
|
||
let e = 5;
|
||
}
|
||
}
|
||
"
|
||
),
|
||
3,
|
||
cx,
|
||
)
|
||
.await,
|
||
Some((
|
||
1..4,
|
||
LineIndent {
|
||
tabs: 0,
|
||
spaces: 4,
|
||
line_blank: false,
|
||
}
|
||
))
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_summaries_for_anchors(cx: &mut TestAppContext) {
|
||
let base_text_1 = indoc!(
|
||
"
|
||
bar
|
||
"
|
||
);
|
||
let text_1 = indoc!(
|
||
"
|
||
BAR
|
||
"
|
||
);
|
||
let base_text_2 = indoc!(
|
||
"
|
||
foo
|
||
"
|
||
);
|
||
let text_2 = indoc!(
|
||
"
|
||
FOO
|
||
"
|
||
);
|
||
|
||
let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
|
||
let buffer_2 = cx.new(|cx| Buffer::local(text_2, cx));
|
||
let diff_1 = cx.new(|cx| {
|
||
BufferDiff::new_with_base_text(base_text_1, &buffer_1.read(cx).text_snapshot(), cx)
|
||
});
|
||
let diff_2 = cx.new(|cx| {
|
||
BufferDiff::new_with_base_text(base_text_2, &buffer_2.read(cx).text_snapshot(), cx)
|
||
});
|
||
cx.run_until_parked();
|
||
|
||
let multibuffer = cx.new(|cx| {
|
||
let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
|
||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(0),
|
||
buffer_1.clone(),
|
||
[Point::zero()..buffer_1.read(cx).max_point()],
|
||
0,
|
||
cx,
|
||
);
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(1),
|
||
buffer_2.clone(),
|
||
[Point::zero()..buffer_2.read(cx).max_point()],
|
||
0,
|
||
cx,
|
||
);
|
||
multibuffer.add_diff(diff_1.clone(), cx);
|
||
multibuffer.add_diff(diff_2.clone(), cx);
|
||
multibuffer
|
||
});
|
||
|
||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||
(multibuffer.snapshot(cx), multibuffer.subscribe())
|
||
});
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
- bar
|
||
+ BAR
|
||
|
||
- foo
|
||
+ FOO
|
||
"
|
||
),
|
||
);
|
||
|
||
let anchor_1 = multibuffer.read_with(cx, |multibuffer, cx| {
|
||
multibuffer
|
||
.snapshot(cx)
|
||
.anchor_in_excerpt(text::Anchor::min_for_buffer(buffer_1.read(cx).remote_id()))
|
||
.unwrap()
|
||
});
|
||
let point_1 = snapshot.summaries_for_anchors::<Point, _>([&anchor_1])[0];
|
||
assert_eq!(point_1, Point::new(0, 0));
|
||
|
||
let anchor_2 = multibuffer.read_with(cx, |multibuffer, cx| {
|
||
multibuffer
|
||
.snapshot(cx)
|
||
.anchor_in_excerpt(text::Anchor::min_for_buffer(buffer_2.read(cx).remote_id()))
|
||
.unwrap()
|
||
});
|
||
let point_2 = snapshot.summaries_for_anchors::<Point, _>([&anchor_2])[0];
|
||
assert_eq!(point_2, Point::new(3, 0));
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_trailing_deletion_without_newline(cx: &mut TestAppContext) {
|
||
let base_text_1 = "one\ntwo".to_owned();
|
||
let text_1 = "one\n".to_owned();
|
||
|
||
let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
|
||
let diff_1 = cx.new(|cx| {
|
||
BufferDiff::new_with_base_text(&base_text_1, &buffer_1.read(cx).text_snapshot(), cx)
|
||
});
|
||
cx.run_until_parked();
|
||
|
||
let multibuffer = cx.new(|cx| {
|
||
let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(0),
|
||
buffer_1.clone(),
|
||
[Point::zero()..buffer_1.read(cx).max_point()],
|
||
0,
|
||
cx,
|
||
);
|
||
multibuffer.add_diff(diff_1.clone(), cx);
|
||
multibuffer.expand_diff_hunks(vec![Anchor::Min..Anchor::Max], cx);
|
||
multibuffer
|
||
});
|
||
|
||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||
(multibuffer.snapshot(cx), multibuffer.subscribe())
|
||
});
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
one
|
||
- two
|
||
"
|
||
),
|
||
);
|
||
|
||
assert_eq!(snapshot.max_point(), Point::new(2, 0));
|
||
assert_eq!(snapshot.len().0, 8);
|
||
|
||
assert_eq!(
|
||
snapshot
|
||
.dimensions_from_points::<Point>([Point::new(2, 0)])
|
||
.collect::<Vec<_>>(),
|
||
vec![Point::new(2, 0)]
|
||
);
|
||
|
||
let (_, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
|
||
assert_eq!(translated_offset.0, "one\n".len());
|
||
let (_, translated_point) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
|
||
assert_eq!(translated_point, Point::new(1, 0));
|
||
|
||
// The same, for an excerpt that's not at the end of the multibuffer.
|
||
|
||
let text_2 = "foo\n".to_owned();
|
||
let buffer_2 = cx.new(|cx| Buffer::local(&text_2, cx));
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpt_ranges_for_path(
|
||
PathKey::sorted(1),
|
||
buffer_2.clone(),
|
||
&buffer_2.read(cx).snapshot(),
|
||
vec![ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
|
||
cx,
|
||
);
|
||
});
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
one
|
||
- two
|
||
|
||
foo
|
||
"
|
||
),
|
||
);
|
||
|
||
assert_eq!(
|
||
snapshot
|
||
.dimensions_from_points::<Point>([Point::new(2, 0)])
|
||
.collect::<Vec<_>>(),
|
||
vec![Point::new(2, 0)]
|
||
);
|
||
|
||
let buffer_1_id = buffer_1.read_with(cx, |buffer_1, _| buffer_1.remote_id());
|
||
let (buffer, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
|
||
assert_eq!(buffer.remote_id(), buffer_1_id);
|
||
assert_eq!(translated_offset.0, "one\n".len());
|
||
let (buffer, translated_point) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
|
||
assert_eq!(buffer.remote_id(), buffer_1_id);
|
||
assert_eq!(translated_point, Point::new(1, 0));
|
||
}
|
||
|
||
fn format_diff(
|
||
text: &str,
|
||
row_infos: &Vec<RowInfo>,
|
||
boundary_rows: &HashSet<MultiBufferRow>,
|
||
has_diff: Option<bool>,
|
||
) -> String {
|
||
let has_diff =
|
||
has_diff.unwrap_or_else(|| row_infos.iter().any(|info| info.diff_status.is_some()));
|
||
text.split('\n')
|
||
.enumerate()
|
||
.zip(row_infos)
|
||
.map(|((ix, line), info)| {
|
||
let marker = match info.diff_status.map(|status| status.kind) {
|
||
Some(DiffHunkStatusKind::Added) => "+ ",
|
||
Some(DiffHunkStatusKind::Deleted) => "- ",
|
||
Some(DiffHunkStatusKind::Modified) => unreachable!(),
|
||
None => {
|
||
if has_diff && !line.is_empty() {
|
||
" "
|
||
} else {
|
||
""
|
||
}
|
||
}
|
||
};
|
||
let boundary_row = if boundary_rows.contains(&MultiBufferRow(ix as u32)) {
|
||
if has_diff {
|
||
" ----------\n"
|
||
} else {
|
||
"---------\n"
|
||
}
|
||
} else {
|
||
""
|
||
};
|
||
let expand = info
|
||
.expand_info
|
||
.as_ref()
|
||
.map(|expand_info| match expand_info.direction {
|
||
ExpandExcerptDirection::Up => " [↑]",
|
||
ExpandExcerptDirection::Down => " [↓]",
|
||
ExpandExcerptDirection::UpAndDown => " [↕]",
|
||
})
|
||
.unwrap_or_default();
|
||
|
||
format!("{boundary_row}{marker}{line}{expand}")
|
||
// let mbr = info
|
||
// .multibuffer_row
|
||
// .map(|row| format!("{:0>3}", row.0))
|
||
// .unwrap_or_else(|| "???".to_string());
|
||
// let byte_range = format!("{byte_range_start:0>3}..{byte_range_end:0>3}");
|
||
// format!("{boundary_row}Row: {mbr}, Bytes: {byte_range} | {marker}{line}{expand}")
|
||
})
|
||
.collect::<Vec<_>>()
|
||
.join("\n")
|
||
}
|
||
|
||
// fn format_transforms(snapshot: &MultiBufferSnapshot) -> String {
|
||
// snapshot
|
||
// .diff_transforms
|
||
// .iter()
|
||
// .map(|transform| {
|
||
// let (kind, summary) = match transform {
|
||
// DiffTransform::DeletedHunk { summary, .. } => (" Deleted", (*summary).into()),
|
||
// DiffTransform::FilteredInsertedHunk { summary, .. } => (" Filtered", *summary),
|
||
// DiffTransform::InsertedHunk { summary, .. } => (" Inserted", *summary),
|
||
// DiffTransform::Unmodified { summary, .. } => ("Unmodified", *summary),
|
||
// };
|
||
// format!("{kind}(len: {}, lines: {:?})", summary.len, summary.lines)
|
||
// })
|
||
// .join("\n")
|
||
// }
|
||
|
||
// fn format_excerpts(snapshot: &MultiBufferSnapshot) -> String {
|
||
// snapshot
|
||
// .excerpts
|
||
// .iter()
|
||
// .map(|excerpt| {
|
||
// format!(
|
||
// "Excerpt(buffer_range = {:?}, lines = {:?}, has_trailing_newline = {:?})",
|
||
// excerpt.range.context.to_point(&excerpt.buffer),
|
||
// excerpt.text_summary.lines,
|
||
// excerpt.has_trailing_newline
|
||
// )
|
||
// })
|
||
// .join("\n")
|
||
// }
|
||
|
||
#[gpui::test]
|
||
async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
|
||
let text = indoc!(
|
||
"
|
||
ZERO
|
||
one
|
||
TWO
|
||
three
|
||
six
|
||
"
|
||
);
|
||
let base_text = indoc!(
|
||
"
|
||
one
|
||
two
|
||
three
|
||
four
|
||
five
|
||
six
|
||
"
|
||
);
|
||
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||
let diff = cx
|
||
.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
|
||
cx.run_until_parked();
|
||
|
||
let base_text_buffer = diff.read_with(cx, |diff, _| diff.base_text_buffer().clone());
|
||
|
||
let multibuffer = cx.new(|cx| {
|
||
let mut multibuffer = MultiBuffer::singleton(base_text_buffer.clone(), cx);
|
||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||
multibuffer.add_inverted_diff(diff.clone(), buffer.clone(), cx);
|
||
multibuffer
|
||
});
|
||
|
||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||
(multibuffer.snapshot(cx), multibuffer.subscribe())
|
||
});
|
||
|
||
assert_eq!(snapshot.text(), base_text);
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
one
|
||
- two
|
||
three
|
||
- four
|
||
- five
|
||
six
|
||
"
|
||
),
|
||
);
|
||
|
||
buffer.update(cx, |buffer, cx| {
|
||
buffer.edit_via_marked_text(
|
||
indoc!(
|
||
"
|
||
ZERO
|
||
one
|
||
«<inserted>»W«O
|
||
T»hree
|
||
six
|
||
"
|
||
),
|
||
None,
|
||
cx,
|
||
);
|
||
});
|
||
cx.run_until_parked();
|
||
let base_text_snapshot = diff.read_with(cx, |diff, cx| diff.base_text(cx));
|
||
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
||
let update = diff
|
||
.update(cx, |diff, cx| {
|
||
diff.update_diff(
|
||
buffer_snapshot,
|
||
&base_text_snapshot,
|
||
Some(Arc::from(base_text)),
|
||
cx,
|
||
)
|
||
})
|
||
.await;
|
||
diff.update(cx, |diff, cx| diff.set_snapshot(update, cx));
|
||
cx.run_until_parked();
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc! {
|
||
"
|
||
one
|
||
- two
|
||
- three
|
||
- four
|
||
- five
|
||
six
|
||
"
|
||
},
|
||
);
|
||
|
||
buffer.update(cx, |buffer, cx| {
|
||
buffer.set_text("ZERO\nONE\nTWO\n", cx);
|
||
});
|
||
cx.run_until_parked();
|
||
let base_text_snapshot = diff.read_with(cx, |diff, cx| diff.base_text(cx));
|
||
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
||
let update = diff
|
||
.update(cx, |diff, cx| {
|
||
diff.update_diff(
|
||
buffer_snapshot,
|
||
&base_text_snapshot,
|
||
Some(Arc::from(base_text)),
|
||
cx,
|
||
)
|
||
})
|
||
.await;
|
||
diff.update(cx, |diff, cx| diff.set_snapshot(update, cx));
|
||
cx.run_until_parked();
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc! {
|
||
"
|
||
- one
|
||
- two
|
||
- three
|
||
- four
|
||
- five
|
||
- six
|
||
"
|
||
},
|
||
);
|
||
|
||
diff.update(cx, |diff, cx| {
|
||
diff.set_base_text(
|
||
Some("new base\n".into()),
|
||
buffer.read(cx).text_snapshot(),
|
||
cx,
|
||
)
|
||
})
|
||
.await;
|
||
cx.run_until_parked();
|
||
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc! {"
|
||
- new base
|
||
"},
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_inverted_diff_base_text_change(cx: &mut TestAppContext) {
|
||
let base_text = "aaa\nbbb\nccc\n";
|
||
let text = "ddd\n";
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||
let diff = cx
|
||
.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
|
||
cx.run_until_parked();
|
||
|
||
let base_text_buffer = diff.read_with(cx, |diff, _| diff.base_text_buffer().clone());
|
||
|
||
let multibuffer = cx.new(|cx| {
|
||
let mut multibuffer = MultiBuffer::singleton(base_text_buffer.clone(), cx);
|
||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||
multibuffer.add_inverted_diff(diff.clone(), buffer.clone(), cx);
|
||
multibuffer
|
||
});
|
||
|
||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||
(multibuffer.snapshot(cx), multibuffer.subscribe())
|
||
});
|
||
|
||
assert_eq!(snapshot.text(), base_text);
|
||
assert_new_snapshot(
|
||
&multibuffer,
|
||
&mut snapshot,
|
||
&mut subscription,
|
||
cx,
|
||
indoc!(
|
||
"
|
||
- aaa
|
||
- bbb
|
||
- ccc
|
||
"
|
||
),
|
||
);
|
||
|
||
diff.update(cx, |diff, cx| {
|
||
diff.set_base_text(Some("ddd\n".into()), buffer.read(cx).text_snapshot(), cx)
|
||
})
|
||
.await;
|
||
|
||
let _hunks: Vec<_> = multibuffer
|
||
.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx))
|
||
.diff_hunks()
|
||
.collect();
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_inverted_diff_secondary_version_mismatch(cx: &mut TestAppContext) {
|
||
let base_text = "one\ntwo\nthree\nfour\nfive\n";
|
||
let index_text = "one\nTWO\nthree\nfour\nfive\n";
|
||
let buffer_text = "one\nTWO\nthree\nFOUR\nfive\n";
|
||
|
||
let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
|
||
|
||
let unstaged_diff = cx
|
||
.new(|cx| BufferDiff::new_with_base_text(index_text, &buffer.read(cx).text_snapshot(), cx));
|
||
cx.run_until_parked();
|
||
|
||
let uncommitted_diff = cx.new(|cx| {
|
||
let mut diff =
|
||
BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx);
|
||
diff.set_secondary_diff(unstaged_diff.clone());
|
||
diff
|
||
});
|
||
cx.run_until_parked();
|
||
|
||
buffer.update(cx, |buffer, cx| {
|
||
buffer.edit([(0..0, "ZERO\n")], None, cx);
|
||
});
|
||
|
||
let base_text_snapshot = unstaged_diff.read_with(cx, |diff, cx| diff.base_text(cx));
|
||
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
||
let update = unstaged_diff
|
||
.update(cx, |diff, cx| {
|
||
diff.update_diff(
|
||
buffer_snapshot,
|
||
&base_text_snapshot,
|
||
Some(Arc::from(index_text)),
|
||
cx,
|
||
)
|
||
})
|
||
.await;
|
||
unstaged_diff.update(cx, |diff, cx| diff.set_snapshot(update, cx));
|
||
|
||
let base_text_buffer =
|
||
uncommitted_diff.read_with(cx, |diff, _| diff.base_text_buffer().clone());
|
||
|
||
let multibuffer = cx.new(|cx| {
|
||
let mut multibuffer = MultiBuffer::singleton(base_text_buffer.clone(), cx);
|
||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||
multibuffer.add_inverted_diff(uncommitted_diff.clone(), buffer.clone(), cx);
|
||
multibuffer
|
||
});
|
||
|
||
let _hunks: Vec<_> = multibuffer
|
||
.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx))
|
||
.diff_hunks()
|
||
.collect();
|
||
}
|
||
|
||
#[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| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
for excerpt in multibuffer.snapshot(cx).excerpts() {
|
||
output.push_str("-----\n");
|
||
output.extend(
|
||
snapshot
|
||
.buffer_for_id(excerpt.context.start.buffer_id)
|
||
.unwrap()
|
||
.text_for_range(excerpt.context),
|
||
);
|
||
if !output.ends_with('\n') {
|
||
output.push('\n');
|
||
}
|
||
}
|
||
});
|
||
assert_eq!(output, expected);
|
||
}
|
||
|
||
#[track_caller]
|
||
fn assert_new_snapshot(
|
||
multibuffer: &Entity<MultiBuffer>,
|
||
snapshot: &mut MultiBufferSnapshot,
|
||
subscription: &mut Subscription<MultiBufferOffset>,
|
||
cx: &mut TestAppContext,
|
||
expected_diff: &str,
|
||
) {
|
||
let new_snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||
let actual_text = new_snapshot.text();
|
||
let line_infos = new_snapshot
|
||
.row_infos(MultiBufferRow(0))
|
||
.collect::<Vec<_>>();
|
||
let actual_diff = format_diff(&actual_text, &line_infos, &Default::default(), None);
|
||
pretty_assertions::assert_eq!(actual_diff, expected_diff);
|
||
check_edits(
|
||
snapshot,
|
||
&new_snapshot,
|
||
&subscription.consume().into_inner(),
|
||
);
|
||
*snapshot = new_snapshot;
|
||
}
|
||
|
||
#[track_caller]
|
||
fn check_edits(
|
||
old_snapshot: &MultiBufferSnapshot,
|
||
new_snapshot: &MultiBufferSnapshot,
|
||
edits: &[Edit<MultiBufferOffset>],
|
||
) {
|
||
let mut text = old_snapshot.text();
|
||
let new_text = new_snapshot.text();
|
||
for edit in edits.iter().rev() {
|
||
if !text.is_char_boundary(edit.old.start.0)
|
||
|| !text.is_char_boundary(edit.old.end.0)
|
||
|| !new_text.is_char_boundary(edit.new.start.0)
|
||
|| !new_text.is_char_boundary(edit.new.end.0)
|
||
{
|
||
panic!(
|
||
"invalid edits: {:?}\nold text: {:?}\nnew text: {:?}",
|
||
edits, text, new_text
|
||
);
|
||
}
|
||
|
||
text.replace_range(
|
||
edit.old.start.0..edit.old.end.0,
|
||
&new_text[edit.new.start.0..edit.new.end.0],
|
||
);
|
||
}
|
||
|
||
pretty_assertions::assert_eq!(text, new_text, "invalid edits: {:?}", edits);
|
||
}
|
||
|
||
#[track_caller]
|
||
fn assert_chunks_in_ranges(snapshot: &MultiBufferSnapshot) {
|
||
let full_text = snapshot.text();
|
||
for ix in 0..full_text.len() {
|
||
let mut chunks = snapshot.chunks(
|
||
MultiBufferOffset(0)..snapshot.len(),
|
||
LanguageAwareStyling {
|
||
tree_sitter: false,
|
||
diagnostics: false,
|
||
},
|
||
);
|
||
chunks.seek(MultiBufferOffset(ix)..snapshot.len());
|
||
let tail = chunks.map(|chunk| chunk.text).collect::<String>();
|
||
assert_eq!(tail, &full_text[ix..], "seek to range: {:?}", ix..);
|
||
}
|
||
}
|
||
|
||
#[track_caller]
|
||
fn assert_consistent_line_numbers(snapshot: &MultiBufferSnapshot) {
|
||
let all_line_numbers = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
|
||
for start_row in 1..all_line_numbers.len() {
|
||
let line_numbers = snapshot
|
||
.row_infos(MultiBufferRow(start_row as u32))
|
||
.collect::<Vec<_>>();
|
||
assert_eq!(
|
||
line_numbers,
|
||
all_line_numbers[start_row..],
|
||
"start_row: {start_row}"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[track_caller]
|
||
fn assert_position_translation(snapshot: &MultiBufferSnapshot) {
|
||
let text = Rope::from(snapshot.text());
|
||
|
||
let mut left_anchors = Vec::new();
|
||
let mut right_anchors = Vec::new();
|
||
let mut offsets = Vec::new();
|
||
let mut points = Vec::new();
|
||
for offset in 0..=text.len() + 1 {
|
||
let offset = MultiBufferOffset(offset);
|
||
let clipped_left = snapshot.clip_offset(offset, Bias::Left);
|
||
let clipped_right = snapshot.clip_offset(offset, Bias::Right);
|
||
assert_eq!(
|
||
clipped_left.0,
|
||
text.clip_offset(offset.0, Bias::Left),
|
||
"clip_offset({offset:?}, Left)"
|
||
);
|
||
assert_eq!(
|
||
clipped_right.0,
|
||
text.clip_offset(offset.0, Bias::Right),
|
||
"clip_offset({offset:?}, Right)"
|
||
);
|
||
assert_eq!(
|
||
snapshot.offset_to_point(clipped_left),
|
||
text.offset_to_point(clipped_left.0),
|
||
"offset_to_point({})",
|
||
clipped_left.0
|
||
);
|
||
assert_eq!(
|
||
snapshot.offset_to_point(clipped_right),
|
||
text.offset_to_point(clipped_right.0),
|
||
"offset_to_point({})",
|
||
clipped_right.0
|
||
);
|
||
let anchor_after = snapshot.anchor_after(clipped_left);
|
||
assert_eq!(
|
||
anchor_after.to_offset(snapshot),
|
||
clipped_left,
|
||
"anchor_after({}).to_offset {anchor_after:?}",
|
||
clipped_left.0
|
||
);
|
||
let anchor_before = snapshot.anchor_before(clipped_left);
|
||
assert_eq!(
|
||
anchor_before.to_offset(snapshot),
|
||
clipped_left,
|
||
"anchor_before({}).to_offset",
|
||
clipped_left.0
|
||
);
|
||
left_anchors.push(anchor_before);
|
||
right_anchors.push(anchor_after);
|
||
offsets.push(clipped_left);
|
||
points.push(text.offset_to_point(clipped_left.0));
|
||
}
|
||
|
||
for row in 0..text.max_point().row {
|
||
for column in 0..text.line_len(row) + 1 {
|
||
let point = Point { row, column };
|
||
let clipped_left = snapshot.clip_point(point, Bias::Left);
|
||
let clipped_right = snapshot.clip_point(point, Bias::Right);
|
||
assert_eq!(
|
||
clipped_left,
|
||
text.clip_point(point, Bias::Left),
|
||
"clip_point({point:?}, Left)"
|
||
);
|
||
assert_eq!(
|
||
clipped_right,
|
||
text.clip_point(point, Bias::Right),
|
||
"clip_point({point:?}, Right)"
|
||
);
|
||
assert_eq!(
|
||
snapshot.point_to_offset(clipped_left).0,
|
||
text.point_to_offset(clipped_left),
|
||
"point_to_offset({clipped_left:?})"
|
||
);
|
||
assert_eq!(
|
||
snapshot.point_to_offset(clipped_right).0,
|
||
text.point_to_offset(clipped_right),
|
||
"point_to_offset({clipped_right:?})"
|
||
);
|
||
}
|
||
}
|
||
|
||
assert_eq!(
|
||
snapshot.summaries_for_anchors::<MultiBufferOffset, _>(&left_anchors),
|
||
offsets,
|
||
"left_anchors <-> offsets"
|
||
);
|
||
assert_eq!(
|
||
snapshot.summaries_for_anchors::<Point, _>(&left_anchors),
|
||
points,
|
||
"left_anchors <-> points"
|
||
);
|
||
assert_eq!(
|
||
snapshot.summaries_for_anchors::<MultiBufferOffset, _>(&right_anchors),
|
||
offsets,
|
||
"right_anchors <-> offsets"
|
||
);
|
||
assert_eq!(
|
||
snapshot.summaries_for_anchors::<Point, _>(&right_anchors),
|
||
points,
|
||
"right_anchors <-> points"
|
||
);
|
||
|
||
for (anchors, bias) in [(&left_anchors, Bias::Left), (&right_anchors, Bias::Right)] {
|
||
for (ix, (offset, anchor)) in offsets.iter().zip(anchors).enumerate() {
|
||
if ix > 0 && *offset == MultiBufferOffset(252) && offset > &offsets[ix - 1] {
|
||
let prev_anchor = left_anchors[ix - 1];
|
||
assert!(
|
||
anchor.cmp(&prev_anchor, snapshot).is_gt(),
|
||
"anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_gt()",
|
||
offsets[ix],
|
||
offsets[ix - 1],
|
||
);
|
||
assert!(
|
||
prev_anchor.cmp(anchor, snapshot).is_lt(),
|
||
"anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_lt()",
|
||
offsets[ix - 1],
|
||
offsets[ix],
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
if let Some((buffer, offset)) = snapshot.point_to_buffer_offset(snapshot.max_point()) {
|
||
assert!(offset.0 <= buffer.len());
|
||
}
|
||
if let Some((buffer, point)) = snapshot.point_to_buffer_point(snapshot.max_point()) {
|
||
assert!(point <= buffer.max_point());
|
||
}
|
||
}
|
||
|
||
fn assert_line_indents(snapshot: &MultiBufferSnapshot) {
|
||
let max_row = snapshot.max_point().row;
|
||
let buffer_id = snapshot.excerpts().next().unwrap().context.start.buffer_id;
|
||
let text = text::Buffer::new(ReplicaId::LOCAL, buffer_id, snapshot.text());
|
||
let mut line_indents = text
|
||
.line_indents_in_row_range(0..max_row + 1)
|
||
.collect::<Vec<_>>();
|
||
for start_row in 0..snapshot.max_point().row {
|
||
pretty_assertions::assert_eq!(
|
||
snapshot
|
||
.line_indents(MultiBufferRow(start_row), |_| true)
|
||
.map(|(row, indent, _)| (row.0, indent))
|
||
.collect::<Vec<_>>(),
|
||
&line_indents[(start_row as usize)..],
|
||
"line_indents({start_row})"
|
||
);
|
||
}
|
||
|
||
line_indents.reverse();
|
||
pretty_assertions::assert_eq!(
|
||
snapshot
|
||
.reversed_line_indents(MultiBufferRow(max_row), |_| true)
|
||
.map(|(row, indent, _)| (row.0, indent))
|
||
.collect::<Vec<_>>(),
|
||
&line_indents[..],
|
||
"reversed_line_indents({max_row})"
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_new_empty_buffer_uses_untitled_title(cx: &mut App) {
|
||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||
|
||
assert_eq!(multibuffer.read(cx).title(cx), "untitled");
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_new_empty_buffer_uses_untitled_title_when_only_contains_whitespace(cx: &mut App) {
|
||
let buffer = cx.new(|cx| Buffer::local("\n ", cx));
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||
|
||
assert_eq!(multibuffer.read(cx).title(cx), "untitled");
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_new_empty_buffer_takes_first_line_for_title(cx: &mut App) {
|
||
let buffer = cx.new(|cx| Buffer::local("Hello World\nSecond line", cx));
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||
|
||
assert_eq!(multibuffer.read(cx).title(cx), "Hello World");
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_new_empty_buffer_takes_trimmed_first_line_for_title(cx: &mut App) {
|
||
let buffer = cx.new(|cx| Buffer::local("\nHello, World ", cx));
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||
|
||
assert_eq!(multibuffer.read(cx).title(cx), "Hello, World");
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_new_empty_buffer_uses_truncated_first_line_for_title(cx: &mut App) {
|
||
let title = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee";
|
||
let title_after = "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd";
|
||
let buffer = cx.new(|cx| Buffer::local(title, cx));
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||
|
||
assert_eq!(multibuffer.read(cx).title(cx), title_after);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_new_empty_buffer_uses_truncated_first_line_for_title_after_merging_adjacent_spaces(
|
||
cx: &mut App,
|
||
) {
|
||
let title = "aaaaaaaaaabbbbbbbbbb ccccccccccddddddddddeeeeeeeeee";
|
||
let title_after = "aaaaaaaaaabbbbbbbbbb ccccccccccddddddddd";
|
||
let buffer = cx.new(|cx| Buffer::local(title, cx));
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||
|
||
assert_eq!(multibuffer.read(cx).title(cx), title_after);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_new_empty_buffers_title_can_be_set(cx: &mut App) {
|
||
let buffer = cx.new(|cx| Buffer::local("Hello World", cx));
|
||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||
assert_eq!(multibuffer.read(cx).title(cx), "Hello World");
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_title("Hey".into(), cx)
|
||
});
|
||
assert_eq!(multibuffer.read(cx).title(cx), "Hey");
|
||
}
|
||
|
||
#[gpui::test(iterations = 100)]
|
||
fn test_random_chunk_bitmaps(cx: &mut App, mut rng: StdRng) {
|
||
let multibuffer = if rng.random() {
|
||
let len = rng.random_range(0..10000);
|
||
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||
cx.new(|cx| MultiBuffer::singleton(buffer, cx))
|
||
} else {
|
||
MultiBuffer::build_random(&mut rng, cx)
|
||
};
|
||
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
|
||
let chunks = snapshot.chunks(
|
||
MultiBufferOffset(0)..snapshot.len(),
|
||
LanguageAwareStyling {
|
||
tree_sitter: false,
|
||
diagnostics: false,
|
||
},
|
||
);
|
||
|
||
for chunk in chunks {
|
||
let chunk_text = chunk.text;
|
||
let chars_bitmap = chunk.chars;
|
||
let tabs_bitmap = chunk.tabs;
|
||
|
||
if chunk_text.is_empty() {
|
||
assert_eq!(
|
||
chars_bitmap, 0,
|
||
"Empty chunk should have empty chars bitmap"
|
||
);
|
||
assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
|
||
continue;
|
||
}
|
||
|
||
assert!(
|
||
chunk_text.len() <= 128,
|
||
"Chunk text length {} exceeds 128 bytes",
|
||
chunk_text.len()
|
||
);
|
||
|
||
// Verify chars bitmap
|
||
let char_indices = chunk_text
|
||
.char_indices()
|
||
.map(|(i, _)| i)
|
||
.collect::<Vec<_>>();
|
||
|
||
for byte_idx in 0..chunk_text.len() {
|
||
let should_have_bit = char_indices.contains(&byte_idx);
|
||
let has_bit = chars_bitmap & (1 << byte_idx) != 0;
|
||
|
||
if has_bit != should_have_bit {
|
||
eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
|
||
eprintln!("Char indices: {:?}", char_indices);
|
||
eprintln!("Chars bitmap: {:#b}", chars_bitmap);
|
||
}
|
||
|
||
assert_eq!(
|
||
has_bit, should_have_bit,
|
||
"Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
|
||
byte_idx, chunk_text, should_have_bit, has_bit
|
||
);
|
||
}
|
||
|
||
for (byte_idx, byte) in chunk_text.bytes().enumerate() {
|
||
let is_tab = byte == b'\t';
|
||
let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
|
||
|
||
if has_bit != is_tab {
|
||
eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
|
||
eprintln!("Tabs bitmap: {:#b}", tabs_bitmap);
|
||
assert_eq!(
|
||
has_bit, is_tab,
|
||
"Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
|
||
byte_idx, chunk_text, byte as char, is_tab, has_bit
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[gpui::test(iterations = 10)]
|
||
fn test_random_chunk_bitmaps_with_diffs(cx: &mut App, mut rng: StdRng) {
|
||
let settings_store = SettingsStore::test(cx);
|
||
cx.set_global(settings_store);
|
||
use buffer_diff::BufferDiff;
|
||
use util::RandomCharIter;
|
||
|
||
let multibuffer = if rng.random() {
|
||
let len = rng.random_range(100..10000);
|
||
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||
cx.new(|cx| MultiBuffer::singleton(buffer, cx))
|
||
} else {
|
||
MultiBuffer::build_random(&mut rng, cx)
|
||
};
|
||
|
||
let _diff_count = rng.random_range(1..5);
|
||
let mut diffs = Vec::new();
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
for buffer_id in snapshot.all_buffer_ids() {
|
||
if rng.random_bool(0.7) {
|
||
if let Some(buffer_handle) = multibuffer.buffer(buffer_id) {
|
||
let buffer_text = buffer_handle.read(cx).text();
|
||
let mut base_text = String::new();
|
||
|
||
for line in buffer_text.lines() {
|
||
if rng.random_bool(0.3) {
|
||
continue;
|
||
} else if rng.random_bool(0.3) {
|
||
let line_len = rng.random_range(0..50);
|
||
let modified_line = RandomCharIter::new(&mut rng)
|
||
.take(line_len)
|
||
.collect::<String>();
|
||
base_text.push_str(&modified_line);
|
||
base_text.push('\n');
|
||
} else {
|
||
base_text.push_str(line);
|
||
base_text.push('\n');
|
||
}
|
||
}
|
||
|
||
if rng.random_bool(0.5) {
|
||
let extra_lines = rng.random_range(1..5);
|
||
for _ in 0..extra_lines {
|
||
let line_len = rng.random_range(0..50);
|
||
let extra_line = RandomCharIter::new(&mut rng)
|
||
.take(line_len)
|
||
.collect::<String>();
|
||
base_text.push_str(&extra_line);
|
||
base_text.push('\n');
|
||
}
|
||
}
|
||
|
||
let diff = cx.new(|cx| {
|
||
BufferDiff::new_with_base_text(
|
||
&base_text,
|
||
&buffer_handle.read(cx).text_snapshot(),
|
||
cx,
|
||
)
|
||
});
|
||
diffs.push(diff.clone());
|
||
multibuffer.add_diff(diff, cx);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
if rng.random_bool(0.5) {
|
||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||
} else {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
let text = snapshot.text();
|
||
|
||
let mut ranges = Vec::new();
|
||
for _ in 0..rng.random_range(1..5) {
|
||
if snapshot.len().0 == 0 {
|
||
break;
|
||
}
|
||
|
||
let diff_size = rng.random_range(5..1000);
|
||
let mut start = rng.random_range(0..snapshot.len().0);
|
||
|
||
while !text.is_char_boundary(start) {
|
||
start = start.saturating_sub(1);
|
||
}
|
||
|
||
let mut end = rng.random_range(start..snapshot.len().0.min(start + diff_size));
|
||
|
||
while !text.is_char_boundary(end) {
|
||
end = end.saturating_add(1);
|
||
}
|
||
let start_anchor = snapshot.anchor_after(MultiBufferOffset(start));
|
||
let end_anchor = snapshot.anchor_before(MultiBufferOffset(end));
|
||
ranges.push(start_anchor..end_anchor);
|
||
}
|
||
multibuffer.expand_diff_hunks(ranges, cx);
|
||
}
|
||
});
|
||
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
|
||
let chunks = snapshot.chunks(
|
||
MultiBufferOffset(0)..snapshot.len(),
|
||
LanguageAwareStyling {
|
||
tree_sitter: false,
|
||
diagnostics: false,
|
||
},
|
||
);
|
||
|
||
for chunk in chunks {
|
||
let chunk_text = chunk.text;
|
||
let chars_bitmap = chunk.chars;
|
||
let tabs_bitmap = chunk.tabs;
|
||
|
||
if chunk_text.is_empty() {
|
||
assert_eq!(
|
||
chars_bitmap, 0,
|
||
"Empty chunk should have empty chars bitmap"
|
||
);
|
||
assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
|
||
continue;
|
||
}
|
||
|
||
assert!(
|
||
chunk_text.len() <= 128,
|
||
"Chunk text length {} exceeds 128 bytes",
|
||
chunk_text.len()
|
||
);
|
||
|
||
let char_indices = chunk_text
|
||
.char_indices()
|
||
.map(|(i, _)| i)
|
||
.collect::<Vec<_>>();
|
||
|
||
for byte_idx in 0..chunk_text.len() {
|
||
let should_have_bit = char_indices.contains(&byte_idx);
|
||
let has_bit = chars_bitmap & (1 << byte_idx) != 0;
|
||
|
||
if has_bit != should_have_bit {
|
||
eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
|
||
eprintln!("Char indices: {:?}", char_indices);
|
||
eprintln!("Chars bitmap: {:#b}", chars_bitmap);
|
||
}
|
||
|
||
assert_eq!(
|
||
has_bit, should_have_bit,
|
||
"Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
|
||
byte_idx, chunk_text, should_have_bit, has_bit
|
||
);
|
||
}
|
||
|
||
for (byte_idx, byte) in chunk_text.bytes().enumerate() {
|
||
let is_tab = byte == b'\t';
|
||
let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
|
||
|
||
if has_bit != is_tab {
|
||
eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
|
||
eprintln!("Tabs bitmap: {:#b}", tabs_bitmap);
|
||
assert_eq!(
|
||
has_bit, is_tab,
|
||
"Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
|
||
byte_idx, chunk_text, byte as char, is_tab, has_bit
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn collect_word_diffs(
|
||
base_text: &str,
|
||
modified_text: &str,
|
||
cx: &mut TestAppContext,
|
||
) -> Vec<String> {
|
||
let buffer = cx.new(|cx| Buffer::local(modified_text, cx));
|
||
let diff = cx
|
||
.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
|
||
cx.run_until_parked();
|
||
|
||
let multibuffer = cx.new(|cx| {
|
||
let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
|
||
multibuffer.add_diff(diff.clone(), cx);
|
||
multibuffer
|
||
});
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.expand_diff_hunks(vec![Anchor::Min..Anchor::Max], cx);
|
||
});
|
||
|
||
let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||
let text = snapshot.text();
|
||
|
||
snapshot
|
||
.diff_hunks()
|
||
.flat_map(|hunk| hunk.word_diffs)
|
||
.map(|range| text[range.start.0..range.end.0].to_string())
|
||
.collect()
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_word_diff_simple_replacement(cx: &mut TestAppContext) {
|
||
let settings_store = cx.update(|cx| SettingsStore::test(cx));
|
||
cx.set_global(settings_store);
|
||
|
||
let base_text = "hello world foo bar\n";
|
||
let modified_text = "hello WORLD foo BAR\n";
|
||
|
||
let word_diffs = collect_word_diffs(base_text, modified_text, cx);
|
||
|
||
assert_eq!(word_diffs, vec!["world", "bar", "WORLD", "BAR"]);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_word_diff_white_space(cx: &mut TestAppContext) {
|
||
let settings_store = cx.update(|cx| SettingsStore::test(cx));
|
||
cx.set_global(settings_store);
|
||
|
||
let base_text = "hello world foo bar\n";
|
||
let modified_text = " hello world foo bar\n";
|
||
|
||
let word_diffs = collect_word_diffs(base_text, modified_text, cx);
|
||
|
||
assert_eq!(word_diffs, vec![" "]);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_word_diff_consecutive_modified_lines(cx: &mut TestAppContext) {
|
||
let settings_store = cx.update(|cx| SettingsStore::test(cx));
|
||
cx.set_global(settings_store);
|
||
|
||
let base_text = "aaa bbb\nccc ddd\n";
|
||
let modified_text = "aaa BBB\nccc DDD\n";
|
||
|
||
let word_diffs = collect_word_diffs(base_text, modified_text, cx);
|
||
|
||
assert_eq!(
|
||
word_diffs,
|
||
vec!["bbb", "ddd", "BBB", "DDD"],
|
||
"consecutive modified lines should produce word diffs when line counts match"
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_word_diff_modified_lines_with_deletion_between(cx: &mut TestAppContext) {
|
||
let settings_store = cx.update(|cx| SettingsStore::test(cx));
|
||
cx.set_global(settings_store);
|
||
|
||
let base_text = "aaa bbb\ndeleted line\nccc ddd\n";
|
||
let modified_text = "aaa BBB\nccc DDD\n";
|
||
|
||
let word_diffs = collect_word_diffs(base_text, modified_text, cx);
|
||
|
||
assert_eq!(
|
||
word_diffs,
|
||
Vec::<String>::new(),
|
||
"modified lines with a deleted line between should not produce word diffs"
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_word_diff_disabled(cx: &mut TestAppContext) {
|
||
let settings_store = cx.update(|cx| {
|
||
let mut settings_store = SettingsStore::test(cx);
|
||
settings_store.update_user_settings(cx, |settings| {
|
||
settings.project.all_languages.defaults.word_diff_enabled = Some(false);
|
||
});
|
||
settings_store
|
||
});
|
||
cx.set_global(settings_store);
|
||
|
||
let base_text = "hello world\n";
|
||
let modified_text = "hello WORLD\n";
|
||
|
||
let word_diffs = collect_word_diffs(base_text, modified_text, cx);
|
||
|
||
assert_eq!(
|
||
word_diffs,
|
||
Vec::<String>::new(),
|
||
"word diffs should be empty when disabled"
|
||
);
|
||
}
|
||
|
||
/// Tests `excerpt_containing` and `excerpts_for_range` (functions mapping multi-buffer text-coordinates to excerpts)
|
||
#[gpui::test]
|
||
fn test_excerpts_containment_functions(cx: &mut App) {
|
||
// Multibuffer content for these tests:
|
||
// 0123
|
||
// 0: aa0
|
||
// 1: aa1
|
||
// -----
|
||
// 2: bb0
|
||
// 3: bb1
|
||
// -----MultiBufferOffset(0)..
|
||
// 4: cc0
|
||
|
||
let buffer_1 = cx.new(|cx| Buffer::local("aa0\naa1", cx));
|
||
let buffer_2 = cx.new(|cx| Buffer::local("bb0\nbb1", cx));
|
||
let buffer_3 = cx.new(|cx| Buffer::local("cc0", cx));
|
||
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
|
||
let (excerpt_1_info, excerpt_2_info, excerpt_3_info) =
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(0),
|
||
buffer_1.clone(),
|
||
[Point::new(0, 0)..Point::new(1, 3)],
|
||
0,
|
||
cx,
|
||
);
|
||
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(1),
|
||
buffer_2.clone(),
|
||
[Point::new(0, 0)..Point::new(1, 3)],
|
||
0,
|
||
cx,
|
||
);
|
||
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(2),
|
||
buffer_3.clone(),
|
||
[Point::new(0, 0)..Point::new(0, 3)],
|
||
0,
|
||
cx,
|
||
);
|
||
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
let mut excerpts = snapshot.excerpts();
|
||
(
|
||
excerpts.next().unwrap(),
|
||
excerpts.next().unwrap(),
|
||
excerpts.next().unwrap(),
|
||
)
|
||
});
|
||
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
|
||
assert_eq!(snapshot.text(), "aa0\naa1\nbb0\nbb1\ncc0");
|
||
|
||
//// Test `excerpts_for_range`
|
||
|
||
let p00 = snapshot.point_to_offset(Point::new(0, 0));
|
||
let p10 = snapshot.point_to_offset(Point::new(1, 0));
|
||
let p20 = snapshot.point_to_offset(Point::new(2, 0));
|
||
let p23 = snapshot.point_to_offset(Point::new(2, 3));
|
||
let p13 = snapshot.point_to_offset(Point::new(1, 3));
|
||
let p40 = snapshot.point_to_offset(Point::new(4, 0));
|
||
let p43 = snapshot.point_to_offset(Point::new(4, 3));
|
||
|
||
let excerpts: Vec<_> = snapshot.excerpts_for_range(p00..p00).collect();
|
||
assert_eq!(excerpts.len(), 1);
|
||
assert_eq!(excerpts[0].range, excerpt_1_info);
|
||
|
||
// Cursor at very end of excerpt 3
|
||
let excerpts: Vec<_> = snapshot.excerpts_for_range(p43..p43).collect();
|
||
assert_eq!(excerpts.len(), 1);
|
||
assert_eq!(excerpts[0].range, excerpt_3_info);
|
||
|
||
let excerpts: Vec<_> = snapshot.excerpts_for_range(p00..p23).collect();
|
||
assert_eq!(excerpts.len(), 2);
|
||
assert_eq!(excerpts[0].range, excerpt_1_info);
|
||
assert_eq!(excerpts[1].range, excerpt_2_info);
|
||
|
||
// This range represent an selection with end-point just inside excerpt_2
|
||
// Today we only expand the first excerpt, but another interpretation that
|
||
// we could consider is expanding both here
|
||
let excerpts: Vec<_> = snapshot.excerpts_for_range(p10..p20).collect();
|
||
assert_eq!(excerpts.len(), 1);
|
||
assert_eq!(excerpts[0].range, excerpt_1_info);
|
||
|
||
//// Test that `excerpts_for_range` and `excerpt_containing` agree for all single offsets (cursor positions)
|
||
for offset in 0..=snapshot.len().0 {
|
||
let offset = MultiBufferOffset(offset);
|
||
let excerpts_for_range: Vec<_> = snapshot.excerpts_for_range(offset..offset).collect();
|
||
assert_eq!(
|
||
excerpts_for_range.len(),
|
||
1,
|
||
"Expected exactly one excerpt for offset {offset}",
|
||
);
|
||
|
||
let (_, excerpt_containing) =
|
||
snapshot
|
||
.excerpt_containing(offset..offset)
|
||
.unwrap_or_else(|| {
|
||
panic!("Expected excerpt_containing to find excerpt for offset {offset}")
|
||
});
|
||
|
||
assert_eq!(
|
||
excerpts_for_range[0].range, excerpt_containing,
|
||
"excerpts_for_range and excerpt_containing should agree for offset {offset}",
|
||
);
|
||
}
|
||
|
||
//// Test `excerpt_containing` behavior with ranges:
|
||
|
||
// Ranges intersecting a single-excerpt
|
||
let (_, containing) = snapshot.excerpt_containing(p00..p13).unwrap();
|
||
assert_eq!(containing, excerpt_1_info);
|
||
|
||
// Ranges intersecting multiple excerpts (should return None)
|
||
let containing = snapshot.excerpt_containing(p20..p40);
|
||
assert!(
|
||
containing.is_none(),
|
||
"excerpt_containing should return None for ranges spanning multiple excerpts"
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_range_to_buffer_ranges(cx: &mut App) {
|
||
let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb", cx));
|
||
let buffer_2 = cx.new(|cx| Buffer::local("ccc", cx));
|
||
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(0),
|
||
buffer_1.clone(),
|
||
[Point::new(0, 0)..Point::new(1, 3)],
|
||
0,
|
||
cx,
|
||
);
|
||
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(1),
|
||
buffer_2.clone(),
|
||
[Point::new(0, 0)..Point::new(0, 3)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
assert_eq!(snapshot.text(), "aaa\nbbb\nccc");
|
||
|
||
let excerpt_2_start = Point::new(2, 0);
|
||
|
||
let ranges_half_open = snapshot.range_to_buffer_ranges(Point::zero()..excerpt_2_start);
|
||
assert_eq!(
|
||
ranges_half_open.len(),
|
||
1,
|
||
"Half-open range ending at excerpt start should EXCLUDE that excerpt"
|
||
);
|
||
assert_eq!(ranges_half_open[0].1, BufferOffset(0)..BufferOffset(7));
|
||
assert_eq!(
|
||
ranges_half_open[0].0.remote_id(),
|
||
buffer_1.read(cx).remote_id()
|
||
);
|
||
|
||
let buffer_empty = cx.new(|cx| Buffer::local("", cx));
|
||
let multibuffer_trailing_empty = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
let (_te_excerpt_1_info, _te_excerpt_2_info) =
|
||
multibuffer_trailing_empty.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(0),
|
||
buffer_1.clone(),
|
||
[Point::new(0, 0)..Point::new(1, 3)],
|
||
0,
|
||
cx,
|
||
);
|
||
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(1),
|
||
buffer_empty.clone(),
|
||
[Point::new(0, 0)..Point::new(0, 0)],
|
||
0,
|
||
cx,
|
||
);
|
||
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
let mut infos = snapshot.excerpts();
|
||
(infos.next().unwrap(), infos.next().unwrap())
|
||
});
|
||
|
||
let snapshot_trailing = multibuffer_trailing_empty.read(cx).snapshot(cx);
|
||
assert_eq!(snapshot_trailing.text(), "aaa\nbbb\n");
|
||
|
||
let max_point = snapshot_trailing.max_point();
|
||
|
||
let ranges_half_open_max = snapshot_trailing.range_to_buffer_ranges(Point::zero()..max_point);
|
||
assert_eq!(
|
||
ranges_half_open_max.len(),
|
||
2,
|
||
"Should include trailing empty excerpts"
|
||
);
|
||
assert_eq!(ranges_half_open_max[1].1, BufferOffset(0)..BufferOffset(0));
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_range_to_buffer_ranges_zero_length_at_excerpt_boundary(cx: &mut App) {
|
||
let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb", cx));
|
||
let buffer_2 = cx.new(|cx| Buffer::local("ccc\nddd", cx));
|
||
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(0),
|
||
buffer_1.clone(),
|
||
[Point::new(0, 0)..Point::new(1, 3)],
|
||
0,
|
||
cx,
|
||
);
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(1),
|
||
buffer_2.clone(),
|
||
[Point::new(0, 0)..Point::new(1, 3)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||
assert_eq!(snapshot.text(), "aaa\nbbb\nccc\nddd");
|
||
|
||
// This point is right at the start of the very first excerpt, so if we get
|
||
// a buffer range, we should get `0..0`
|
||
let excerpt_2_start = Point::new(2, 0);
|
||
let expected_ranges = vec![BufferOffset(0)..BufferOffset(0)];
|
||
let ranges = snapshot
|
||
.range_to_buffer_ranges(excerpt_2_start..excerpt_2_start)
|
||
.into_iter()
|
||
.map(|tup| tup.1)
|
||
.collect_vec();
|
||
|
||
assert_eq!(
|
||
ranges, expected_ranges,
|
||
"Zero-length range at excerpt boundary should return the excerpt at that point"
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_buffer_range_to_excerpt_ranges(cx: &mut TestAppContext) {
|
||
let base_text = indoc!(
|
||
"
|
||
aaa
|
||
bbb
|
||
ccc
|
||
ddd
|
||
eee
|
||
ppp
|
||
qqq
|
||
rrr
|
||
fff
|
||
ggg
|
||
hhh
|
||
"
|
||
);
|
||
let text = indoc!(
|
||
"
|
||
aaa
|
||
BBB
|
||
ddd
|
||
eee
|
||
ppp
|
||
qqq
|
||
rrr
|
||
FFF
|
||
ggg
|
||
hhh
|
||
"
|
||
);
|
||
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||
let diff = cx
|
||
.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
|
||
cx.run_until_parked();
|
||
|
||
let multibuffer = cx.new(|cx| {
|
||
let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
|
||
multibuffer.set_excerpts_for_path(
|
||
PathKey::sorted(0),
|
||
buffer.clone(),
|
||
[
|
||
Point::new(0, 0)..Point::new(3, 3),
|
||
Point::new(7, 0)..Point::new(9, 3),
|
||
],
|
||
0,
|
||
cx,
|
||
);
|
||
multibuffer.add_diff(diff.clone(), cx);
|
||
multibuffer
|
||
});
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.expand_diff_hunks(vec![Anchor::Min..Anchor::Max], cx);
|
||
});
|
||
cx.run_until_parked();
|
||
|
||
let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||
|
||
let actual_diff = format_diff(
|
||
&snapshot.text(),
|
||
&snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>(),
|
||
&Default::default(),
|
||
None,
|
||
);
|
||
let expected_diff = indoc!(
|
||
"
|
||
aaa
|
||
- bbb
|
||
- ccc
|
||
+ BBB
|
||
ddd
|
||
eee [\u{2193}]
|
||
- fff [\u{2191}]
|
||
+ FFF
|
||
ggg
|
||
hhh [\u{2193}]"
|
||
);
|
||
pretty_assertions::assert_eq!(actual_diff, expected_diff);
|
||
|
||
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||
|
||
let query_spanning_deleted_hunk = buffer_snapshot.anchor_after(Point::new(0, 0))
|
||
..buffer_snapshot.anchor_before(Point::new(1, 3));
|
||
assert_eq!(
|
||
snapshot
|
||
.buffer_range_to_excerpt_ranges(query_spanning_deleted_hunk)
|
||
.map(|range| range.to_point(&snapshot))
|
||
.collect::<Vec<_>>(),
|
||
vec![
|
||
Point::new(0, 0)..Point::new(1, 0),
|
||
Point::new(3, 0)..Point::new(3, 3),
|
||
],
|
||
);
|
||
|
||
let query_within_contiguous_main_buffer = buffer_snapshot.anchor_after(Point::new(1, 0))
|
||
..buffer_snapshot.anchor_before(Point::new(2, 3));
|
||
assert_eq!(
|
||
snapshot
|
||
.buffer_range_to_excerpt_ranges(query_within_contiguous_main_buffer)
|
||
.map(|range| range.to_point(&snapshot))
|
||
.collect::<Vec<_>>(),
|
||
vec![Point::new(3, 0)..Point::new(4, 3)],
|
||
);
|
||
|
||
let query_spanning_both_excerpts = buffer_snapshot.anchor_after(Point::new(2, 0))
|
||
..buffer_snapshot.anchor_before(Point::new(8, 3));
|
||
assert_eq!(
|
||
snapshot
|
||
.buffer_range_to_excerpt_ranges(query_spanning_both_excerpts)
|
||
.map(|range| range.to_point(&snapshot))
|
||
.collect::<Vec<_>>(),
|
||
vec![
|
||
Point::new(4, 0)..Point::new(5, 3),
|
||
Point::new(7, 0)..Point::new(8, 3),
|
||
],
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_cannot_seek_backward_after_excerpt_replacement(cx: &mut TestAppContext) {
|
||
let buffer_b_text: String = (0..50).map(|i| format!("line_b {i}\n")).collect();
|
||
let buffer_b = cx.new(|cx| Buffer::local(buffer_b_text, cx));
|
||
|
||
let buffer_c_text: String = (0..10).map(|i| format!("line_c {i}\n")).collect();
|
||
let buffer_c = cx.new(|cx| Buffer::local(buffer_c_text, cx));
|
||
|
||
let buffer_d_text: String = (0..10).map(|i| format!("line_d {i}\n")).collect();
|
||
let buffer_d = cx.new(|cx| Buffer::local(buffer_d_text, cx));
|
||
|
||
let path_b = PathKey::with_sort_prefix(0, rel_path("bbb.rs").into_arc());
|
||
let path_c = PathKey::with_sort_prefix(0, rel_path("ddd.rs").into_arc());
|
||
let path_d = PathKey::with_sort_prefix(0, rel_path("ccc.rs").into_arc());
|
||
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
path_b.clone(),
|
||
buffer_b.clone(),
|
||
vec![
|
||
Point::row_range(0..3),
|
||
Point::row_range(15..18),
|
||
Point::row_range(30..33),
|
||
],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
path_c.clone(),
|
||
buffer_c.clone(),
|
||
vec![Point::row_range(0..3)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
let (anchor_in_e_b2, anchor_in_e_b3) = multibuffer.read_with(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
let excerpt_infos = snapshot.excerpts().collect::<Vec<_>>();
|
||
assert_eq!(excerpt_infos.len(), 4, "expected 4 excerpts (3×B + 1×C)");
|
||
|
||
let e_b2_info = excerpt_infos[1].clone();
|
||
let e_b3_info = excerpt_infos[2].clone();
|
||
|
||
let anchor_b2 = snapshot.anchor_in_excerpt(e_b2_info.context.start).unwrap();
|
||
let anchor_b3 = snapshot.anchor_in_excerpt(e_b3_info.context.start).unwrap();
|
||
(anchor_b2, anchor_b3)
|
||
});
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
path_b.clone(),
|
||
buffer_b.clone(),
|
||
vec![Point::row_range(0..3), Point::row_range(28..36)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
path_d.clone(),
|
||
buffer_d.clone(),
|
||
vec![Point::row_range(0..3)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
multibuffer.read_with(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
snapshot.summaries_for_anchors::<Point, _>(&[anchor_in_e_b2, anchor_in_e_b3]);
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_resolving_max_anchor_for_buffer(cx: &mut TestAppContext) {
|
||
let dock_base_text = indoc! {"
|
||
0
|
||
1
|
||
2
|
||
3
|
||
4
|
||
5
|
||
6
|
||
7
|
||
8
|
||
9
|
||
10
|
||
11
|
||
12
|
||
"};
|
||
|
||
let dock_text = indoc! {"
|
||
0
|
||
4
|
||
5
|
||
6
|
||
10
|
||
11
|
||
12
|
||
"};
|
||
|
||
let dock_buffer = cx.new(|cx| Buffer::local(dock_text, cx));
|
||
let diff = cx.new(|cx| {
|
||
BufferDiff::new_with_base_text(dock_base_text, &dock_buffer.read(cx).snapshot(), cx)
|
||
});
|
||
|
||
let workspace_text = "second buffer\n";
|
||
let workspace_buffer = cx.new(|cx| Buffer::local(workspace_text, cx));
|
||
|
||
let dock_path = PathKey::with_sort_prefix(0, rel_path("").into_arc());
|
||
let workspace_path = PathKey::with_sort_prefix(1, rel_path("").into_arc());
|
||
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpt_ranges_for_path(
|
||
dock_path,
|
||
dock_buffer.clone(),
|
||
&dock_buffer.read(cx).snapshot(),
|
||
vec![
|
||
ExcerptRange::new(Point::zero()..Point::new(1, 1)),
|
||
ExcerptRange::new(Point::new(3, 0)..Point::new(4, 2)),
|
||
],
|
||
cx,
|
||
);
|
||
multibuffer.set_excerpt_ranges_for_path(
|
||
workspace_path,
|
||
workspace_buffer.clone(),
|
||
&workspace_buffer.read(cx).snapshot(),
|
||
vec![ExcerptRange::new(
|
||
Point::zero()..workspace_buffer.read(cx).max_point(),
|
||
)],
|
||
cx,
|
||
);
|
||
multibuffer.add_diff(diff, cx);
|
||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||
});
|
||
|
||
let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||
let diff = format_diff(
|
||
&snapshot.text(),
|
||
&snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>(),
|
||
&Default::default(),
|
||
None,
|
||
);
|
||
assert_eq!(
|
||
diff,
|
||
indoc! {"
|
||
0
|
||
- 1
|
||
- 2
|
||
- 3
|
||
4 [↓]
|
||
6 [↑]
|
||
- 7
|
||
- 8
|
||
- 9
|
||
10 [↓]
|
||
second buffer
|
||
"}
|
||
);
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
let point = snapshot
|
||
.anchor_in_buffer(text::Anchor::max_for_buffer(
|
||
dock_buffer.read(cx).remote_id(),
|
||
))
|
||
.unwrap()
|
||
.to_point(&snapshot);
|
||
assert_eq!(point, Point::new(10, 0));
|
||
})
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_is_valid_anchor_past_last_excerpt_for_buffer(cx: &mut TestAppContext) {
|
||
let buffer_a = cx.new(|cx| Buffer::local("aaa\nbbb\nccc\n", cx));
|
||
buffer_a.update(cx, |buffer, cx| {
|
||
let len = buffer.len();
|
||
buffer.edit([(len..len, "ddd\neee\n")], None, cx);
|
||
});
|
||
let buffer_b = cx.new(|cx| Buffer::local("xxx\n", cx));
|
||
for line in ["yyy\n", "zzz\n", "www\n", "vvv\n"] {
|
||
buffer_b.update(cx, |buffer, cx| {
|
||
let len = buffer.len();
|
||
buffer.edit([(len..len, line)], None, cx);
|
||
});
|
||
}
|
||
|
||
let path_a = PathKey::with_sort_prefix(0, rel_path("aaa.rs").into_arc());
|
||
let path_b = PathKey::with_sort_prefix(1, rel_path("bbb.rs").into_arc());
|
||
|
||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||
|
||
multibuffer.update(cx, |multibuffer, cx| {
|
||
multibuffer.set_excerpts_for_path(
|
||
path_a.clone(),
|
||
buffer_a.clone(),
|
||
vec![Point::new(1, 0)..Point::new(2, 3)],
|
||
0,
|
||
cx,
|
||
);
|
||
multibuffer.set_excerpts_for_path(
|
||
path_b.clone(),
|
||
buffer_b.clone(),
|
||
vec![Point::new(1, 0)..Point::new(3, 3)],
|
||
0,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
multibuffer.read_with(cx, |multibuffer, cx| {
|
||
let snapshot = multibuffer.snapshot(cx);
|
||
|
||
let buffer_a_snapshot = buffer_a.read(cx).snapshot();
|
||
let anchor_past_excerpt = buffer_a_snapshot.anchor_after(Point::new(4, 0));
|
||
let mb_anchor = snapshot.anchor_in_buffer(anchor_past_excerpt).unwrap();
|
||
|
||
assert!(
|
||
!mb_anchor.is_valid(&snapshot),
|
||
"anchor past the last excerpt for its buffer should not be valid"
|
||
);
|
||
});
|
||
}
|