Files
zed/crates/multi_buffer/src/multi_buffer_tests.rs
T
Mohamad KhaniandClaude Fable 5 b72a46db68
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree
with a 3-file patch applied (no upstream history).

Patch (crates/gpui_linux/src/linux/wayland/):
  - serial.rs: add SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; add latest_serial_of()
  - window.rs: activate() uses keyboard-enter serial (Mutter focus gate)

Mutter honors window activation only when the token carries the keyboard-
focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial.
See docs/tray-window-focus-wayland.md in logiguard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 02:22:17 +03:30

6260 lines
193 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::*;
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
use gpui::{App, TestAppContext};
use indoc::indoc;
use language::{Buffer, Rope};
use parking_lot::RwLock;
use rand::prelude::*;
use settings::SettingsStore;
use std::env;
use std::time::{Duration, Instant};
use util::RandomCharIter;
use util::rel_path::rel_path;
use util::test::sample_text;
#[ctor::ctor]
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,
is_local: true,
},
Event::Edited {
edited_buffer: None,
is_local: true,
},
Event::Edited {
edited_buffer: None,
is_local: true,
}
]
);
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]
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 update = diff
.update(cx, |diff, cx| {
diff.update_diff(
buffer.read(cx).text_snapshot(),
Some(base_text.into()),
None,
None,
cx,
)
})
.await;
diff.update(cx, |diff, cx| {
diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx)
})
.await;
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 update = diff
.update(cx, |diff, cx| {
diff.update_diff(
buffer.read(cx).text_snapshot(),
Some(base_text.into()),
None,
None,
cx,
)
})
.await;
diff.update(cx, |diff, cx| {
diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx)
})
.await;
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()),
None,
buffer.read(cx).text_snapshot(),
cx,
)
})
.await
.unwrap();
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
"
),
);
let update = diff
.update(cx, |diff, cx| {
diff.update_diff(
buffer.read(cx).text_snapshot(),
Some("ddd\n".into()),
Some(true),
None,
cx,
)
})
.await;
diff.update(cx, |diff, cx| {
diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx)
})
.detach();
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 update = unstaged_diff
.update(cx, |diff, cx| {
diff.update_diff(
buffer.read(cx).text_snapshot(),
Some(index_text.into()),
None,
None,
cx,
)
})
.await;
unstaged_diff
.update(cx, |diff, cx| {
diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx)
})
.await;
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"
);
});
}