Files
galaxy/app/src/editor/view/model/buffer/mod_test.rs
T

4456 lines
162 KiB
Rust

// Allow the `single_range_in_vec_init` clippy rule. The buffer#edit API takes in a `Vec` of ranges,
// which is at odds with this clippy rule.
#![allow(clippy::single_range_in_vec_init)]
use crate::editor::{soft_wrap::ClampDirection, tests::RandomCharIter};
use async_channel::Receiver;
use test::Network;
use super::*;
use enclose::enclose;
use futures::StreamExt;
use rand::prelude::StdRng;
use std::{
cmp::Ordering,
collections::HashSet,
pin::{pin, Pin},
};
use warpui::{color::ColorU, App, ModelHandle};
fn visible_text_styles(buffer: &Buffer) -> Vec<Option<TextStyle>> {
buffer
.fragments
.items()
.into_iter()
.filter_map(|fragment| {
fragment
.is_visible(&buffer.undo_history)
.then_some(fragment.text.text_style)
})
.collect()
}
type OpsReceiver = Pin<Box<Receiver<Rc<Vec<Operation>>>>>;
/// Creates a new [`Buffer`], registers it as a model in the App
/// and creates a proxy receiver for any [`Event::UpdatePeers`] events
/// emitted by the buffer.
fn new_buffer_with_ops_receiver(
app: &mut App,
replica_id: ReplicaId,
text: &str,
) -> (ModelHandle<Buffer>, OpsReceiver) {
let model = app.add_model(|_| Buffer::new_with_replica_id(replica_id, text));
let (tx, rx) = async_channel::unbounded();
app.update(|ctx| {
ctx.subscribe_to_model(&model, move |_, event, _| {
if let Event::UpdatePeers { operations } = event {
tx.try_send(operations.clone()).expect("can send message");
}
});
});
(model, Box::pin(rx))
}
impl ToCharOffset for usize {
fn to_char_offset(&self, _: &Buffer) -> Result<CharOffset> {
Ok(CharOffset::from(*self))
}
}
fn to_char_index_range(range: Range<usize>) -> Range<CharOffset> {
CharOffset::from(range.start)..CharOffset::from(range.end)
}
#[test]
#[should_panic]
fn test_nested_batches_are_not_allowed() {
App::test((), |mut app| async move {
let buffer = app.add_model(|_| Buffer::new(""));
buffer.update(&mut app, |buffer, _ctx| {
// The buffer should not start in a batch.
assert!(!buffer.batch_state.is_batching());
// Start a batch.
buffer.start_selection_changes_only_batch();
// The buffer is in a batching state.
assert!(buffer.batch_state.is_batching());
// Start another batch. This should panic -- nested batches are not allowed.
buffer.start_selection_changes_only_batch();
})
})
}
#[test]
#[should_panic]
fn test_edit_without_batching() {
App::test((), |mut app| async move {
let buffer = app.add_model(|_| Buffer::new(""));
buffer.update(&mut app, |buffer, ctx| {
// The buffer should not start in a batch.
assert!(!buffer.batch_state.is_batching());
// Try to edit. This should panic.
let _ = buffer.edit(vec![to_char_index_range(0..0)], "a", ctx);
})
})
}
#[test]
#[should_panic]
fn test_change_selections_without_batching() {
App::test((), |mut app| async move {
let buffer = app.add_model(|_| Buffer::new(""));
buffer.update(&mut app, |buffer, _| {
// The buffer should not start in a batch.
assert!(!buffer.batch_state.is_batching());
// Try to change selections. This should panic.
buffer
.change_selections(
vec1![LocalSelection::new_for_test(Anchor::Start, Anchor::End)].into(),
)
.unwrap();
})
})
}
#[test]
fn test_change_selections() {
App::test((), |mut app| async move {
let buffer = app.add_model(|_| Buffer::new("abc"));
let new_selections: LocalSelections = vec1![LocalSelection {
selection: Selection {
start: Anchor::End,
end: Anchor::Start,
reversed: true,
},
clamp_direction: ClampDirection::Up,
goal_end_column: None,
goal_start_column: None,
}]
.into();
buffer.read(&app, |buffer, _| {
assert_ne!(buffer.local_selections, new_selections);
});
buffer.update(&mut app, |buffer, ctx| {
buffer.start_selection_changes_only_batch();
buffer.change_selections(new_selections.clone()).unwrap();
buffer.end_batch(ctx);
});
buffer.read(&app, |buffer, _| {
assert_eq!(buffer.local_selections, new_selections);
});
})
}
#[test]
fn test_merge_local_selections() {
App::test((), |mut app| async move {
let buffer = app.add_model(|_| Buffer::new("abcde"));
let (b_anchor, d_anchor, e_anchor) = buffer.read(&app, |buffer, _ctx| {
(
buffer.anchor_before(1).unwrap(),
buffer.anchor_before(3).unwrap(),
buffer.anchor_after(4).unwrap(),
)
});
// Create two contiguous selections of "bc" and "de".
buffer.update(&mut app, |buffer, _| {
buffer.local_selections = LocalSelections {
pending: None,
selections: vec1![
LocalSelection::new_for_test(b_anchor.clone(), d_anchor.clone()),
LocalSelection::new_for_test(d_anchor.clone(), e_anchor.clone()),
],
marked_text_state: Default::default(),
}
});
buffer.update(&mut app, |buffer, ctx| {
buffer.start_selection_changes_only_batch();
buffer.merge_local_selections().unwrap();
buffer.end_batch(ctx);
});
// After merging, we should just have one selection of "bcde".
buffer.read(&app, |buffer, _| {
assert_eq!(
buffer.local_selections,
LocalSelections {
pending: None,
selections: vec1![LocalSelection::new_for_test(
b_anchor.clone(),
e_anchor.clone()
),],
marked_text_state: Default::default()
}
)
});
})
}
#[test]
fn test_edit_for_test() -> Result<()> {
App::test((), |mut app| async move {
let buffer = app.add_model(|_| Buffer::new("abc"));
buffer.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "abc");
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
"def",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "abcdef");
buffer.edit_for_test(
vec![to_char_index_range(0..0)],
"ghi",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "ghiabcdef");
buffer.edit_for_test(
vec![to_char_index_range(5..5)],
"jkl",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "ghiabjklcdef");
buffer.edit_for_test(
vec![to_char_index_range(6..7)],
"",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "ghiabjlcdef");
buffer.edit_for_test(
vec![to_char_index_range(4..9)],
"mno",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "ghiamnoef");
Ok(())
})
})
}
#[test]
fn test_edit_with_text_style() -> Result<()> {
App::test((), |mut app| async move {
let buffer = app.add_model(|_| Buffer::new("abc"));
buffer.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "abc");
let black_highlight_text_style =
TextStyle::default().with_background_color(ColorU::black());
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
Text::new("def", Some(black_highlight_text_style)),
EditOrigin::UserInitiated,
ctx,
)?;
// Ensure the last fragment has the correct text style.
let text_styles = visible_text_styles(buffer);
assert_eq!(
text_styles,
vec![None, None, Some(black_highlight_text_style)]
);
Ok(())
})
})
}
#[test]
fn test_edit_multiple_ranges() -> Result<()> {
App::test((), |mut app| async move {
let buffer = app.add_model(|_| Buffer::new("abcdefghi"));
buffer.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "abcdefghi");
buffer.edit_for_test(
vec![to_char_index_range(1..3), to_char_index_range(5..8)],
"_foo_",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "a_foo_de_foo_i");
Ok(())
})
})
}
#[test]
fn test_edit_multiple_ranges_empty_string() -> Result<()> {
App::test((), |mut app| async move {
let buffer = app.add_model(|_| Buffer::new("abcdefghi"));
buffer.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "abcdefghi");
buffer.edit_for_test(
vec![to_char_index_range(1..3), to_char_index_range(5..8)],
"",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "adei");
Ok(())
})
})
}
#[test]
fn test_update_text_style_entire_run() -> Result<()> {
App::test((), |mut app| async move {
// Initialize buffer with "abc" (no styles).
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new("abc"));
buffer_model.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "abc");
let black_highlight_text_style =
TextStyle::default().with_background_color(ColorU::black());
// We add "def" to the buffer with a black highlight style.
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
Text::new("def", Some(black_highlight_text_style)),
EditOrigin::UserInitiated,
ctx,
)?;
// Ensure the last fragment has the correct text style.
let text_styles = visible_text_styles(buffer);
assert_eq!(
text_styles,
vec![None, None, Some(black_highlight_text_style)]
);
// We update "abc" in the buffer to have a black underline style
// (previously had no styles).
let black_underline_style =
TextStyle::default().with_error_underline_color(ColorU::black());
let black_underline_style_operation =
TextStyleOperation::default().set_error_underline_color(ColorU::black());
buffer.update_styles(
vec![to_char_index_range(0..3)],
black_underline_style_operation,
ctx,
)?;
// Check against all expected text style runs.
let text = buffer.text();
assert_eq!(text, "abcdef");
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![
TextRun::new(
"abc".to_owned(),
black_underline_style,
ByteOffset::from(0)..ByteOffset::from(3)
),
TextRun::new(
"def".to_owned(),
black_highlight_text_style,
ByteOffset::from(3)..ByteOffset::from(6)
)
]
);
Ok(())
})
})
}
/// Test to make sure backspacing logic i.e. deletions, works correctly
/// with updating styles.
#[test]
fn test_update_text_style_clear_style() -> Result<()> {
App::test((), |mut app| async move {
// Initialize a buffer with "abc" (no styles).
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new("abc"));
buffer_model.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "abc");
let white_underline_style =
TextStyle::default().with_error_underline_color(ColorU::white());
// We add "def" to the end of the buffer with a white underline
// (new state is "abcdef").
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
Text::new("def", Some(white_underline_style)),
EditOrigin::UserInitiated,
ctx,
)?;
// Ensure the last fragment has the correct text style.
let text_styles: Vec<_> = visible_text_styles(buffer);
assert_eq!(text_styles, vec![None, None, Some(white_underline_style)]);
let black_underline_style =
TextStyle::default().with_error_underline_color(ColorU::black());
let black_underline_style_operation =
TextStyleOperation::default().set_error_underline_color(ColorU::black());
// We style "abc" with a black underline.
buffer.update_styles(
vec![to_char_index_range(0..3)],
black_underline_style_operation,
ctx,
)?;
// Check the state of the buffer against what we expect.
let text = buffer.text();
assert_eq!(text, "abcdef");
assert_eq!(text.len(), buffer.len().as_usize());
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![
TextRun::new(
"abc".to_owned(),
black_underline_style,
ByteOffset::from(0)..ByteOffset::from(3)
),
TextRun::new(
"def".to_owned(),
white_underline_style,
ByteOffset::from(3)..ByteOffset::from(6)
)
]
);
// We clear the white underline from "def".
// After this, we expect "abcdef" where only "abc" has a black underline.
buffer.update_styles(
vec![to_char_index_range(3..6)],
TextStyleOperation::default().clear_error_underline_color(),
ctx,
)?;
// Check the state of the buffer against what we expect.
let text = buffer.text();
assert_eq!(text, "abcdef");
assert_eq!(text.len(), buffer.len().as_usize());
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![
TextRun::new(
"abc".to_owned(),
black_underline_style,
ByteOffset::from(0)..ByteOffset::from(3)
),
TextRun::new(
"def".to_owned(),
TextStyle::default(),
ByteOffset::from(3)..ByteOffset::from(6)
)
]
);
Ok(())
})
})
}
/// Test style inheritance with edit operations and inheritable/non-inheritable
/// styles.
#[test]
fn test_edit_style_inheritable() -> Result<()> {
App::test((), |mut app| async move {
// Initialize a buffer with "abc" (no styles).
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new("abc"));
buffer_model.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "abc");
// Inheritable background style
let black_background_style =
TextStyle::default().with_background_color(ColorU::black());
// Non-inheritable error underline style
let black_error_underline_style =
TextStyle::default().with_error_underline_color(ColorU::black());
// Add "def" to end of buffer with black background.
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
Text::new("def", Some(black_background_style)),
EditOrigin::UserInitiated,
ctx,
)?;
// Add "ghi" to end of buffer - we expect this to inherit the black
// background style.
buffer.edit_for_test(
vec![to_char_index_range(6..6)],
Text::new("ghi", None),
EditOrigin::UserInitiated,
ctx,
)?;
// Add "jkl" to end of buffer with black underline style.
buffer.edit_for_test(
vec![to_char_index_range(9..9)],
Text::new("jkl", Some(black_error_underline_style)),
EditOrigin::UserInitiated,
ctx,
)?;
// Add "mno" to end of buffer with no style - we don't
// expect it to inherit the black underline style!
buffer.edit_for_test(
vec![to_char_index_range(12..12)],
Text::new("mno", None),
EditOrigin::UserInitiated,
ctx,
)?;
// Check buffer against what we expect.
let text = buffer.text();
assert_eq!(text, "abcdefghijklmno");
assert_eq!(text.len(), buffer.len().as_usize());
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![
TextRun::new(
"abc".to_owned(),
TextStyle::default(),
ByteOffset::from(0)..ByteOffset::from(3)
),
// "ghi" inherited black background from "def"
TextRun::new(
"defghi".to_owned(),
black_background_style,
ByteOffset::from(3)..ByteOffset::from(9)
),
TextRun::new(
"jkl".to_owned(),
black_error_underline_style,
ByteOffset::from(9)..ByteOffset::from(12)
),
// "mno" did not inherit black underline from "jkl"
TextRun::new(
"mno".to_owned(),
TextStyle::default(),
ByteOffset::from(12)..ByteOffset::from(15)
),
]
);
Ok(())
})
})
}
#[test]
fn test_update_text_style_backspace() -> Result<()> {
App::test((), |mut app| async move {
// Initialize an empty buffer.
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new(""));
buffer_model.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "");
let black_underline_style =
TextStyle::default().with_error_underline_color(ColorU::black());
let black_underline_style_operation =
TextStyleOperation::default().set_error_underline_color(ColorU::black());
// Add "g" with no text style to the buffer.
buffer.edit_for_test(
vec![to_char_index_range(0..0)],
Text::new("g", None),
EditOrigin::UserInitiated,
ctx,
)?;
// Update "g" to have a black underline.
buffer.update_styles(
vec![to_char_index_range(0..1)],
black_underline_style_operation,
ctx,
)?;
// Add "i" with no text style to the buffer.
buffer.edit_for_test(
vec![to_char_index_range(1..1)],
Text::new("i", None),
EditOrigin::UserInitiated,
ctx,
)?;
// Update "gi" to have a black underline.
buffer.update_styles(
vec![to_char_index_range(0..2)],
black_underline_style_operation,
ctx,
)?;
// Check whether the state of our buffer matches what we expect.
let text = buffer.text();
assert_eq!(text, "gi");
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![TextRun::new(
"gi".to_owned(),
black_underline_style,
ByteOffset::from(0)..ByteOffset::from(2)
),]
);
// Add a space " " to the end of the buffer.
buffer.edit_for_test(
vec![to_char_index_range(2..2)],
" ",
EditOrigin::UserInitiated,
ctx,
)?;
// Update "gi" to have a black underline (no visible change here).
buffer.update_styles(
vec![to_char_index_range(0..2)],
black_underline_style_operation,
ctx,
)?;
// Add "s" to the end of the buffer.
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
"s",
EditOrigin::UserInitiated,
ctx,
)?;
// Update "gi" to have a black underline (no visible change here).
buffer.update_styles(
vec![to_char_index_range(0..2)],
black_underline_style_operation,
ctx,
)?;
assert_eq!(buffer.text(), "gi s");
// Delete "i s" from the buffer (last 3 characters).
buffer.edit_for_test(
vec![to_char_index_range(1..4)],
"",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "g");
// Add "p" to the end of the buffer
buffer.edit_for_test(
vec![to_char_index_range(1..1)],
"p",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "gp");
// Update "gp" to have a black underline.
buffer.update_styles(
vec![to_char_index_range(0..2)],
black_underline_style_operation,
ctx,
)?;
// Confirm that we deleted fragments correctly - check what is
// in our buffer.
assert_eq!(buffer.text(), "gp");
Ok(())
})
})
}
/// Longer test to test deletions logic in conjunction with splicing
/// and style inheritance.
#[test]
fn test_update_text_style_backspace_splice_inheritance() -> Result<()> {
App::test((), |mut app| async move {
// Initialize an empty buffer.
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new(""));
buffer_model.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "");
// Non-inheritable error underline style.
let black_error_underline_style =
TextStyle::default().with_error_underline_color(ColorU::black());
let black_error_underline_style_operation =
TextStyleOperation::default().set_error_underline_color(ColorU::black());
let green_underline_style =
TextStyle::default().with_error_underline_color(ColorU::new(0, 255, 0, 0));
let green_underline_style_operation =
TextStyleOperation::default().set_error_underline_color(ColorU::new(0, 255, 0, 0));
// Add "g" to end of empty buffer.
buffer.edit_for_test(
vec![to_char_index_range(0..0)],
Text::new("g", None),
EditOrigin::UserInitiated,
ctx,
)?;
// Update "g" to have a black underline.
buffer.update_styles(
vec![to_char_index_range(0..1)],
black_error_underline_style_operation,
ctx,
)?;
assert_eq!(buffer.text(), "g");
// Add "iabcdef" to the end of the buffer. Note that error
// underlining is not inheritable hence they should not
// have a black underline.
buffer.edit_for_test(
vec![to_char_index_range(1..1)],
Text::new("iabcdef", None),
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "giabcdef");
// Update "gia" to have a black underline.
buffer.update_styles(
vec![to_char_index_range(0..3)],
black_error_underline_style_operation,
ctx,
)?;
assert_eq!(buffer.text(), "giabcdef");
// Update "bcd" to have a green underline.
buffer.update_styles(
vec![to_char_index_range(3..6)],
green_underline_style_operation,
ctx,
)?;
// Check the state of the buffer against what we expect (both text
// and styles).
let text = buffer.text();
assert_eq!(text, "giabcdef");
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![
TextRun::new(
"gia".to_owned(),
black_error_underline_style,
ByteOffset::from(0)..ByteOffset::from(3)
),
TextRun::new(
"bcd".to_owned(),
green_underline_style,
ByteOffset::from(3)..ByteOffset::from(6)
),
// "ef" did not inherit the black error underline.
TextRun::new(
"ef".to_owned(),
TextStyle::default(),
ByteOffset::from(6)..ByteOffset::from(8)
),
]
);
// Delete "cde" from the buffer.
buffer.edit_for_test(
vec![to_char_index_range(4..7)],
"",
EditOrigin::UserInitiated,
ctx,
)?;
// Check new state of buffer against what we expect.
assert_eq!(buffer.text(), "giabf");
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![
TextRun::new(
"gia".to_owned(),
black_error_underline_style,
ByteOffset::from(0)..ByteOffset::from(3)
),
TextRun::new(
"b".to_owned(),
green_underline_style,
ByteOffset::from(3)..ByteOffset::from(4)
),
TextRun::new(
"f".to_owned(),
TextStyle::default(),
ByteOffset::from(4)..ByteOffset::from(5)
),
]
);
let blue_foreground_style_operation =
TextStyleOperation::default().set_foreground_color(ColorU::new(0, 0, 255, 0));
// Update "ab" to have a blue foreground color.
buffer.update_styles(
vec![to_char_index_range(2..4)],
blue_foreground_style_operation,
ctx,
)?;
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![
TextRun::new(
"gi".to_owned(),
black_error_underline_style,
ByteOffset::from(0)..ByteOffset::from(2)
),
// "a" has both a black underline and blue foreground color.
TextRun::new(
"a".to_owned(),
black_error_underline_style
.with_foreground_color(ColorU::new(0, 0, 255, 0)),
ByteOffset::from(2)..ByteOffset::from(3)
),
// "b" has both a green underline and blue foreground color.
TextRun::new(
"b".to_owned(),
green_underline_style.with_foreground_color(ColorU::new(0, 0, 255, 0)),
ByteOffset::from(3)..ByteOffset::from(4)
),
TextRun::new(
"f".to_owned(),
TextStyle::default(),
ByteOffset::from(4)..ByteOffset::from(5)
),
]
);
Ok(())
})
})
}
#[test]
fn test_update_text_style_partial_run() -> Result<()> {
App::test((), |mut app| async move {
// Initialize buffer with "abc" (no styles).
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new("abc"));
buffer_model.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "abc");
let black_highlight_text_style =
TextStyle::default().with_background_color(ColorU::black());
// Add "def" to end of buffer with a black highlight style.
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
Text::new("def", Some(black_highlight_text_style)),
EditOrigin::UserInitiated,
ctx,
)?;
// Ensure the last fragment has the correct text style.
let text_styles = visible_text_styles(buffer);
assert_eq!(
text_styles,
vec![None, None, Some(black_highlight_text_style)]
);
let black_underline_style =
TextStyle::default().with_error_underline_color(ColorU::black());
let black_underline_style_operation =
TextStyleOperation::default().set_error_underline_color(ColorU::black());
// Update "ab" to have a black underline.
buffer.update_styles(
vec![to_char_index_range(0..2)],
black_underline_style_operation,
ctx,
)?;
// Check state of buffer against what we expect.
let text = buffer.text();
assert_eq!(text, "abcdef");
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![
TextRun::new(
"ab".to_owned(),
black_underline_style,
ByteOffset::from(0)..ByteOffset::from(2)
),
TextRun::new(
"c".to_owned(),
TextStyle::default(),
ByteOffset::from(2)..ByteOffset::from(3)
),
TextRun::new(
"def".to_owned(),
black_highlight_text_style,
ByteOffset::from(3)..ByteOffset::from(6)
)
]
);
Ok(())
})
})
}
#[test]
fn test_update_text_style_multiple_style_updates() -> Result<()> {
App::test((), |mut app| async move {
// Initialize buffer with "abc" (no styles).
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new("abc"));
buffer_model.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "abc");
let black_highlight_text_style =
TextStyle::default().with_background_color(ColorU::black());
// Add "defghi" to end of buffer with black highlight style.
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
Text::new("defghi", Some(black_highlight_text_style)),
EditOrigin::UserInitiated,
ctx,
)?;
// Ensure the last fragment has the correct text style.
let text_styles = visible_text_styles(buffer);
assert_eq!(
text_styles,
vec![None, None, Some(black_highlight_text_style)]
);
let black_underline_style =
TextStyle::default().with_error_underline_color(ColorU::black());
let black_underline_style_operation =
TextStyleOperation::default().set_error_underline_color(ColorU::black());
let red_underline_style_operation =
TextStyleOperation::default().set_error_underline_color(ColorU::new(255, 0, 0, 0));
// Update "ab" to have a black underline.
buffer.update_styles(
vec![to_char_index_range(0..2)],
black_underline_style_operation,
ctx,
)?;
// Update "fgh" to have a red underline.
buffer.update_styles(
vec![to_char_index_range(5..8)],
red_underline_style_operation,
ctx,
)?;
// Check the current state of the buffer against what we expect.
let text = buffer.text();
assert_eq!(text, "abcdefghi");
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![
TextRun::new(
"ab".to_owned(),
black_underline_style,
ByteOffset::from(0)..ByteOffset::from(2)
),
TextRun::new(
"c".to_owned(),
TextStyle::default(),
ByteOffset::from(2)..ByteOffset::from(3)
),
TextRun::new(
"de".to_owned(),
black_highlight_text_style,
ByteOffset::from(3)..ByteOffset::from(5)
),
// "fgh" should have a black highlight style and red underline.
TextRun::new(
"fgh".to_owned(),
TextStyle::default()
.with_background_color(ColorU::black())
.with_error_underline_color(ColorU::new(255, 0, 0, 0)),
ByteOffset::from(5)..ByteOffset::from(8)
),
TextRun::new(
"i".to_owned(),
black_highlight_text_style,
ByteOffset::from(8)..ByteOffset::from(9)
)
]
);
Ok(())
})
})
}
#[test]
fn test_update_text_style_complex_multiple_ranges() -> Result<()> {
App::test((), |mut app| async move {
// Initialize buffer with "abc" (no styles).
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new("abc"));
buffer_model.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "abc");
let black_highlight_text_style =
TextStyle::default().with_background_color(ColorU::black());
// Add "defghijkl" to the end of the buffer with a black highlight style
// (note highlight == background color).
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
Text::new("defghijkl", Some(black_highlight_text_style)),
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(
text_styles,
vec![None, None, Some(black_highlight_text_style)]
);
let black_underline_style =
TextStyle::default().with_error_underline_color(ColorU::black());
let black_underline_style_operation =
TextStyleOperation::default().set_error_underline_color(ColorU::black());
let red_underline_style_operation =
TextStyleOperation::default().set_error_underline_color(ColorU::new(255, 0, 0, 0));
// Update "ab" to have a black underline.
buffer.update_styles(
vec![to_char_index_range(0..2)],
black_underline_style_operation,
ctx,
)?;
// Update both "fgh" and "jk" to have a red underline i.e. multiple
// ranges at once.
buffer.update_styles(
vec![to_char_index_range(5..8), to_char_index_range(9..11)],
red_underline_style_operation,
ctx,
)?;
// Check state of buffer against what we expect.
let text = buffer.text();
assert_eq!(text, "abcdefghijkl");
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![
TextRun::new(
"ab".to_owned(),
black_underline_style,
ByteOffset::from(0)..ByteOffset::from(2)
),
TextRun::new(
"c".to_owned(),
TextStyle::default(),
ByteOffset::from(2)..ByteOffset::from(3)
),
TextRun::new(
"de".to_owned(),
black_highlight_text_style,
ByteOffset::from(3)..ByteOffset::from(5)
),
// "fgh" should have a black highlight and red underline.
TextRun::new(
"fgh".to_owned(),
TextStyle::default()
.with_background_color(ColorU::black())
.with_error_underline_color(ColorU::new(255, 0, 0, 0)),
ByteOffset::from(5)..ByteOffset::from(8)
),
TextRun::new(
"i".to_owned(),
black_highlight_text_style,
ByteOffset::from(8)..ByteOffset::from(9)
),
// "jk" should have a black highlight and red underline.
TextRun::new(
"jk".to_owned(),
TextStyle::default()
.with_background_color(ColorU::black())
.with_error_underline_color(ColorU::new(255, 0, 0, 0)),
ByteOffset::from(9)..ByteOffset::from(11)
),
TextRun::new(
"l".to_owned(),
black_highlight_text_style,
ByteOffset::from(11)..ByteOffset::from(12)
)
]
);
let green_underline_style =
TextStyle::default().with_error_underline_color(ColorU::new(0, 255, 0, 0));
let green_underline_style_operation =
TextStyleOperation::default().set_error_underline_color(ColorU::new(0, 255, 0, 0));
// Update "abcdefghijkl" (entire buffer) to have a green underline.
buffer.update_styles(
vec![to_char_index_range(0..12)],
green_underline_style_operation,
ctx,
)?;
assert_eq!(text, "abcdefghijkl");
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![
// "ab" had black underline overwritten to green underline.
// "c" also now has a green underline.
TextRun::new(
"abc".to_owned(),
green_underline_style,
ByteOffset::from(0)..ByteOffset::from(3)
),
// "de" and "i" now also have a green underline, in addition
// to black highlight style.
// "fgh" and "jk" had black underline overwritten to green
// underline (along with keeping black highlight).
TextRun::new(
"defghijkl".to_owned(),
TextStyle::default()
.with_background_color(ColorU::black())
.with_error_underline_color(ColorU::new(0, 255, 0, 0),),
ByteOffset::from(3)..ByteOffset::from(12)
),
]
);
Ok(())
})
})
}
#[test]
fn test_update_text_style_one_char() -> Result<()> {
App::test((), |mut app| async move {
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new(""));
buffer_model.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "");
let black_highlight_text_style =
TextStyle::default().with_background_color(ColorU::black());
let black_highlight_text_style_operation =
TextStyleOperation::default().set_background_color(ColorU::black());
buffer.edit_for_test(
vec![to_char_index_range(0..0)],
Text::new("a", None),
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "a");
assert_eq!(buffer.text().len(), buffer.len().as_usize());
buffer.update_styles(
vec![to_char_index_range(0..1)],
black_highlight_text_style_operation,
ctx,
)?;
assert_eq!(buffer.text(), "a");
assert_eq!(1, buffer.len().as_usize());
let text_styles = visible_text_styles(buffer);
assert_eq!(text_styles, vec![None, Some(black_highlight_text_style)]);
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![TextRun::new(
"a".to_owned(),
black_highlight_text_style,
ByteOffset::from(0)..ByteOffset::from(1)
),]
);
buffer.edit_for_test(
vec![to_char_index_range(1..1)],
Text::new("b", None),
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "ab");
assert_eq!(2, buffer.len().as_usize());
Ok(())
})
})
}
#[test]
fn test_text_style_ranges() -> Result<()> {
App::test((), |mut app| async move {
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new("abc"));
buffer_model.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "abc");
let black_highlight_text_style =
TextStyle::default().with_background_color(ColorU::black());
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
Text::new("def", Some(black_highlight_text_style)),
EditOrigin::UserInitiated,
ctx,
)?;
buffer.edit_for_test(
vec![to_char_index_range(6..6)],
Text::new("g", Some(black_highlight_text_style)),
EditOrigin::UserInitiated,
ctx,
)?;
let text = buffer.text();
assert_eq!(text, "abcdefg");
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![
TextRun::new(
"abc".to_owned(),
TextStyle::default(),
ByteOffset::from(0)..ByteOffset::from(3)
),
TextRun::new(
"defg".to_owned(),
black_highlight_text_style,
ByteOffset::from(3)..ByteOffset::from(7)
)
]
);
buffer.edit_for_test(
vec![to_char_index_range(6..6)],
Text::new("123", Some(TextStyle::default())),
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![
TextRun::new(
"abc".to_owned(),
TextStyle::default(),
ByteOffset::from(0)..ByteOffset::from(3)
),
TextRun::new(
"def".to_owned(),
black_highlight_text_style,
ByteOffset::from(3)..ByteOffset::from(6)
),
TextRun::new(
"123".to_owned(),
TextStyle::default(),
ByteOffset::from(6)..ByteOffset::from(9)
),
TextRun::new(
"g".to_owned(),
black_highlight_text_style,
ByteOffset::from(9)..ByteOffset::from(10)
)
]
);
buffer.edit_for_test(
vec![to_char_index_range(0..3)],
Text::new("", None),
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![
TextRun::new(
"def".to_owned(),
black_highlight_text_style,
ByteOffset::from(0)..ByteOffset::from(3)
),
TextRun::new(
"123".to_owned(),
TextStyle::default(),
ByteOffset::from(3)..ByteOffset::from(6)
),
TextRun::new(
"g".to_owned(),
black_highlight_text_style,
ByteOffset::from(6)..ByteOffset::from(7)
)
]
);
buffer.edit_for_test(
vec![to_char_index_range(3..6)],
Text::new("", None),
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(
buffer.text_style_runs().collect::<Vec<_>>(),
vec![TextRun::new(
"defg".to_owned(),
black_highlight_text_style,
ByteOffset::from(0)..ByteOffset::from(4)
)]
);
Ok(())
})
})
}
#[test]
fn test_text_styles_back_to_back() -> Result<()> {
App::test((), |mut app| async move {
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new("ab"));
buffer_model.update(&mut app, |buffer, ctx| {
let red_text_style =
TextStyle::default().with_background_color(ColorU::from_u32(0xFF0000FF));
let green_text_style =
TextStyle::default().with_background_color(ColorU::from_u32(0x00FF00FF));
buffer.edit_for_test(
vec![to_char_index_range(2..2)],
Text::new("cd", Some(red_text_style)),
EditOrigin::UserInitiated,
ctx,
)?;
buffer.edit_for_test(
vec![to_char_index_range(4..4)],
Text::new("ef", Some(green_text_style)),
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "abcdef");
assert_eq!(
text_styles,
vec![None, None, Some(red_text_style), Some(green_text_style)]
);
Ok(())
})
})
}
#[test]
fn test_text_styles_delete_last_word() -> Result<()> {
App::test((), |mut app| async move {
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new(""));
buffer_model.update(&mut app, |buffer, ctx| {
let text_style =
TextStyle::default().with_background_color(ColorU::from_u32(0xFF0000FF));
buffer.edit_for_test(
vec![to_char_index_range(0..0)],
"foo",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "foo");
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
Text::new("bar", Some(text_style)),
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "foobar");
assert_eq!(text_styles, vec![None, None, Some(text_style),]);
buffer.edit_for_test(
vec![to_char_index_range(2..6)],
"",
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "fo");
assert_eq!(text_styles, vec![None, None,]);
buffer.edit_for_test(
vec![to_char_index_range(2..2)],
"b",
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "fob");
assert_eq!(text_styles, vec![None, None, None]);
Ok(())
})
})
}
#[test]
fn test_text_styles_delete_and_replace_whole_fragment() -> Result<()> {
App::test((), |mut app| async move {
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new(""));
buffer_model.update(&mut app, |buffer, ctx| {
let text_style =
TextStyle::default().with_background_color(ColorU::from_u32(0xFF0000FF));
buffer.edit_for_test(
vec![to_char_index_range(0..0)],
"foo",
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "foo");
assert_eq!(text_styles, vec![None, None,]);
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
Text::new("bar", Some(text_style)),
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "foobar");
assert_eq!(text_styles, vec![None, None, Some(text_style),]);
buffer.edit_for_test(
vec![to_char_index_range(4..4)],
"bazz",
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "foobbazzar");
assert_eq!(
text_styles,
vec![
None,
None,
Some(text_style),
Some(text_style),
Some(text_style)
]
);
buffer.edit_for_test(vec![3..10], "a", EditOrigin::UserInitiated, ctx)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "fooa");
assert_eq!(text_styles, vec![None, None, Some(text_style),]);
Ok(())
})
})
}
#[test]
fn test_text_styles_delete_until_end_of_highlight() -> Result<()> {
App::test((), |mut app| async move {
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new(""));
buffer_model.update(&mut app, |buffer, ctx| {
let text_style =
TextStyle::default().with_background_color(ColorU::from_u32(0xFF0000FF));
buffer.edit_for_test(
vec![to_char_index_range(0..0)],
"foo",
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "foo");
assert_eq!(text_styles, vec![None, None,]);
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
Text::new("bar", Some(text_style)),
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "foobar");
assert_eq!(text_styles, vec![None, None, Some(text_style),]);
buffer.edit_for_test(
vec![to_char_index_range(3..6)],
"",
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "foo");
assert_eq!(text_styles, vec![None, None,]);
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
"b",
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "foob");
assert_eq!(text_styles, vec![None, None, Some(text_style),]);
buffer.edit_for_test(
vec![to_char_index_range(3..4)],
"",
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "foo");
assert_eq!(text_styles, vec![None, None]);
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
"a",
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "fooa");
assert_eq!(text_styles, vec![None, None, Some(text_style),]);
buffer.edit_for_test(
vec![to_char_index_range(2..4)],
"",
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "fo");
assert_eq!(text_styles, vec![None, None]);
buffer.edit_for_test(
vec![to_char_index_range(2..2)],
"a",
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "foa");
assert_eq!(text_styles, vec![None, None, None]);
Ok(())
})
})
}
#[test]
fn test_text_styles_delete_until_highlight() -> Result<()> {
App::test((), |mut app| async move {
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new(""));
buffer_model.update(&mut app, |buffer, ctx| {
let text_style =
TextStyle::default().with_background_color(ColorU::from_u32(0xFF0000FF));
buffer.edit_for_test(
vec![to_char_index_range(0..0)],
"foo",
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "foo");
assert_eq!(text_styles, vec![None, None,]);
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
"bar",
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "foobar");
assert_eq!(text_styles, vec![None, None, None]);
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
Text::new("bazz", Some(text_style)),
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "foobazzbar");
assert_eq!(text_styles, vec![None, None, Some(text_style), None,]);
buffer.edit_for_test(vec![7..10], "", EditOrigin::UserInitiated, ctx)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "foobazz");
assert_eq!(text_styles, vec![None, None, Some(text_style),]);
buffer.edit_for_test(
vec![to_char_index_range(8..8)],
"b",
EditOrigin::UserInitiated,
ctx,
)?;
let text_styles = visible_text_styles(buffer);
assert_eq!(buffer.text(), "foobazzb");
assert_eq!(text_styles, vec![None, None, Some(text_style), None,]);
Ok(())
})
})
}
#[test]
fn test_edit_events() {
App::test((), |mut app| async move {
let base_text = "abcdef";
let (buffer_1_tx, buffer_1_rx) = async_channel::unbounded();
let (buffer_2_tx, buffer_2_rx) = async_channel::unbounded();
let buffer1 = app.add_model(|_| Buffer::new_with_replica_id(ReplicaId::new(1), base_text));
let buffer2 = app.add_model(|_| Buffer::new_with_replica_id(ReplicaId::new(2), base_text));
app.update(|ctx| {
ctx.subscribe_to_model(&buffer1, move |_, event, _| {
buffer_1_tx
.try_send(event.to_owned())
.expect("Can send over buffer_2_tx")
});
ctx.subscribe_to_model(&buffer2, move |_, event, _| {
buffer_2_tx
.try_send(event.to_owned())
.expect("Can send over buffer_2_tx")
});
});
buffer1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(Some(2..4), "XYZ", EditOrigin::UserInitiated, ctx)
.unwrap()
});
let mut buffer_1_rx = pin!(buffer_1_rx);
let buffer_1_edit_event = buffer_1_rx
.next()
.await
.expect("buffer 1 has an edited event");
assert_eq!(
buffer_1_edit_event,
Event::Edited {
edits: vec![Edit {
old_range: to_char_index_range(2..4),
new_range: to_char_index_range(2..5)
}],
edit_origin: EditOrigin::UserInitiated,
}
);
let buffer_1_event = buffer_1_rx
.next()
.await
.expect("buffer 1 has an update peers event");
let operations = match buffer_1_event {
Event::UpdatePeers { operations } => operations.to_vec(),
_ => panic!("Expected to receive an UpdatePeers event but got {buffer_1_event:?}"),
};
buffer2.update(&mut app, |buffer, ctx| {
buffer.apply_ops(operations, ctx).unwrap();
});
let mut buffer_2_rx = pin!(buffer_2_rx);
let buffer_2_edit_event = buffer_2_rx
.next()
.await
.expect("buffer 2 has an edited event");
assert_eq!(
buffer_2_edit_event,
Event::Edited {
edits: vec![Edit {
old_range: to_char_index_range(2..4),
new_range: to_char_index_range(2..5)
}],
edit_origin: EditOrigin::RemoteEdit,
}
);
})
}
#[test]
fn test_random_edits() {
App::test((), |mut app| async move {
for seed in 0..100 {
println!("{seed:?}");
let mut rng = &mut StdRng::seed_from_u64(seed);
let reference_string_len = rng.gen_range(0..3);
let mut reference_string = RandomCharIter::new(&mut rng)
.take(reference_string_len)
.collect::<String>();
let buffer = app.add_model(|_| Buffer::new(reference_string.as_str()));
buffer.update(&mut app, |buffer, ctx| {
let mut buffer_versions = Vec::new();
for _i in 0..10 {
let (old_ranges, new_text) = buffer.randomly_edit(
rng,
RangesWhenEditing::UseRandomRanges { num_ranges: 5 },
ctx,
);
for old_range in old_ranges.iter().rev() {
reference_string = [
&reference_string[0..old_range.start.as_usize()],
new_text.as_str(),
&reference_string[old_range.end.as_usize()..],
]
.concat();
}
assert_eq!(buffer.text(), reference_string);
if rng.gen_bool(0.3) {
buffer_versions.push(buffer.clone());
}
}
for mut old_buffer in buffer_versions {
let mut delta = 0_isize;
for Edit {
old_range,
new_range,
} in buffer.edits_since(old_buffer.versions.clone())
{
let old_len = old_range.end - old_range.start;
let new_len = new_range.end - new_range.start;
let old_start = CharOffset::from(
(old_range.start.as_usize() as isize + delta) as usize,
);
old_buffer
.edit_for_test(
Some(old_start..old_start + old_len),
buffer.text_for_range(new_range).unwrap(),
EditOrigin::UserInitiated,
ctx,
)
.unwrap();
delta += new_len.as_usize() as isize - old_len.as_usize() as isize;
}
assert_eq!(old_buffer.text(), buffer.text());
}
})
}
})
}
#[test]
fn test_line_len() -> Result<()> {
App::test((), |mut app| async move {
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new(""));
buffer_model.update(&mut app, |buffer, ctx| {
buffer.edit_for_test(
vec![to_char_index_range(0..0)],
"abcd\nefg\nhij",
EditOrigin::UserInitiated,
ctx,
)?;
buffer.edit_for_test(vec![12..12], "kl\nmno", EditOrigin::UserInitiated, ctx)?;
buffer.edit_for_test(vec![18..18], "\npqrs\n", EditOrigin::UserInitiated, ctx)?;
buffer.edit_for_test(vec![18..21], "\nPQ", EditOrigin::UserInitiated, ctx)?;
assert_eq!(buffer.line_len(0)?, 4);
assert_eq!(buffer.line_len(1)?, 3);
assert_eq!(buffer.line_len(2)?, 5);
assert_eq!(buffer.line_len(3)?, 3);
assert_eq!(buffer.line_len(4)?, 4);
assert_eq!(buffer.line_len(5)?, 0);
assert!(buffer.line_len(6).is_err());
Ok(())
})
})
}
#[test]
fn test_text_summary_for_range() {
let buffer = Buffer::new("ab\nefg\nhklm\nnopqrs\ntuvwxyz");
let text = Text::from(buffer.text());
assert_eq!(
buffer.text_summary_for_range(to_char_index_range(1..3)),
text.slice(to_char_index_range(1..3)).summary()
);
assert_eq!(
buffer.text_summary_for_range(to_char_index_range(1..12)),
text.slice(to_char_index_range(1..12)).summary()
);
assert_eq!(
buffer.text_summary_for_range(to_char_index_range(0..20)),
text.slice(to_char_index_range(0..20)).summary()
);
assert_eq!(
buffer.text_summary_for_range(to_char_index_range(0..22)),
text.slice(to_char_index_range(0..22)).summary()
);
assert_eq!(
buffer.text_summary_for_range(to_char_index_range(7..22)),
text.slice(to_char_index_range(7..22)).summary()
);
}
#[test]
fn test_chars_at() -> Result<()> {
App::test((), |mut app| async move {
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new(""));
buffer_model.update(&mut app, |buffer, ctx| {
buffer.edit_for_test(
vec![to_char_index_range(0..0)],
"abcd\nefgh\nij",
EditOrigin::UserInitiated,
ctx,
)?;
buffer.edit_for_test(vec![12..12], "kl\nmno", EditOrigin::UserInitiated, ctx)?;
buffer.edit_for_test(vec![18..18], "\npqrs", EditOrigin::UserInitiated, ctx)?;
buffer.edit_for_test(vec![18..21], "\nPQ", EditOrigin::UserInitiated, ctx)?;
let chars = buffer.chars_at(Point::new(0, 0))?;
assert_eq!(chars.collect::<String>(), "abcd\nefgh\nijkl\nmno\nPQrs");
let chars = buffer.chars_at(Point::new(1, 0))?;
assert_eq!(chars.collect::<String>(), "efgh\nijkl\nmno\nPQrs");
let chars = buffer.chars_at(Point::new(2, 0))?;
assert_eq!(chars.collect::<String>(), "ijkl\nmno\nPQrs");
let chars = buffer.chars_at(Point::new(3, 0))?;
assert_eq!(chars.collect::<String>(), "mno\nPQrs");
let chars = buffer.chars_at(Point::new(4, 0))?;
assert_eq!(chars.collect::<String>(), "PQrs");
// Regression test:
let mut buffer = Buffer::new("");
buffer.edit_for_test(vec![to_char_index_range(0..0)], "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n", EditOrigin::UserInitiated, ctx)?;
buffer.edit_for_test(vec![60..60], "\n", EditOrigin::UserInitiated, ctx)?;
let chars = buffer.chars_at(Point::new(6, 0))?;
assert_eq!(chars.collect::<String>(), " \"xray_wasm\",\n]\n");
Ok(())
})
})
}
#[test]
fn test_fragment_ids() {
for seed in 0..10 {
let rng = &mut StdRng::seed_from_u64(seed);
let mut ids = vec![FragmentId(Box::new([0])), FragmentId(Box::new([4]))];
for _i in 0..100 {
let index = rng.gen_range(1..ids.len());
let left = ids[index - 1].clone();
let right = ids[index].clone();
ids.insert(index, FragmentId::between_with_max(&left, &right, 4));
let mut sorted_ids = ids.clone();
sorted_ids.sort();
assert_eq!(ids, sorted_ids);
}
}
}
#[test]
fn test_anchors() -> Result<()> {
App::test((), |mut app| async move {
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new(""));
buffer_model.update(&mut app, |buffer, ctx| {
buffer.edit_for_test(
vec![to_char_index_range(0..0)],
"abc",
EditOrigin::UserInitiated,
ctx,
)?;
let left_anchor = buffer.anchor_before(2).unwrap();
let right_anchor = buffer.anchor_after(2).unwrap();
buffer.edit_for_test(
vec![to_char_index_range(1..1)],
"def\n",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "adef\nbc");
assert_eq!(left_anchor.to_char_offset(buffer).unwrap(), 6.into());
assert_eq!(right_anchor.to_char_offset(buffer).unwrap(), 6.into());
assert_eq!(
left_anchor.to_point(buffer).unwrap(),
Point { row: 1, column: 1 }
);
assert_eq!(
right_anchor.to_point(buffer).unwrap(),
Point { row: 1, column: 1 }
);
buffer.edit_for_test(
vec![to_char_index_range(2..3)],
"",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "adf\nbc");
assert_eq!(left_anchor.to_char_offset(buffer).unwrap(), 5.into());
assert_eq!(right_anchor.to_char_offset(buffer).unwrap(), 5.into());
assert_eq!(
left_anchor.to_point(buffer).unwrap(),
Point { row: 1, column: 1 }
);
assert_eq!(
right_anchor.to_point(buffer).unwrap(),
Point { row: 1, column: 1 }
);
buffer.edit_for_test(
vec![to_char_index_range(5..5)],
"ghi\n",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "adf\nbghi\nc");
assert_eq!(left_anchor.to_char_offset(buffer).unwrap(), 5.into());
assert_eq!(right_anchor.to_char_offset(buffer).unwrap(), 9.into());
assert_eq!(
left_anchor.to_point(buffer).unwrap(),
Point { row: 1, column: 1 }
);
assert_eq!(
right_anchor.to_point(buffer).unwrap(),
Point { row: 2, column: 0 }
);
buffer.edit_for_test(
vec![to_char_index_range(7..9)],
"",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "adf\nbghc");
assert_eq!(left_anchor.to_char_offset(buffer).unwrap(), 5.into());
assert_eq!(right_anchor.to_char_offset(buffer).unwrap(), 7.into());
assert_eq!(
left_anchor.to_point(buffer).unwrap(),
Point { row: 1, column: 1 },
);
assert_eq!(
right_anchor.to_point(buffer).unwrap(),
Point { row: 1, column: 3 }
);
// Ensure anchoring to a point is equivalent to anchoring to an offset.
assert_eq!(
buffer.anchor_before(Point { row: 0, column: 0 })?,
buffer.anchor_before(0)?
);
assert_eq!(
buffer.anchor_before(Point { row: 0, column: 1 })?,
buffer.anchor_before(1)?
);
assert_eq!(
buffer.anchor_before(Point { row: 0, column: 2 })?,
buffer.anchor_before(2)?
);
assert_eq!(
buffer.anchor_before(Point { row: 0, column: 3 })?,
buffer.anchor_before(3)?
);
assert_eq!(
buffer.anchor_before(Point { row: 1, column: 0 })?,
buffer.anchor_before(4)?
);
assert_eq!(
buffer.anchor_before(Point { row: 1, column: 1 })?,
buffer.anchor_before(5)?
);
assert_eq!(
buffer.anchor_before(Point { row: 1, column: 2 })?,
buffer.anchor_before(6)?
);
assert_eq!(
buffer.anchor_before(Point { row: 1, column: 3 })?,
buffer.anchor_before(7)?
);
assert_eq!(
buffer.anchor_before(Point { row: 1, column: 4 })?,
buffer.anchor_before(8)?
);
// Comparison between anchors.
let anchor_at_offset_0 = buffer.anchor_before(0).unwrap();
let anchor_at_offset_1 = buffer.anchor_before(1).unwrap();
let anchor_at_offset_2 = buffer.anchor_before(2).unwrap();
assert_eq!(
anchor_at_offset_0.cmp(&anchor_at_offset_0, buffer)?,
Ordering::Equal
);
assert_eq!(
anchor_at_offset_1.cmp(&anchor_at_offset_1, buffer)?,
Ordering::Equal
);
assert_eq!(
anchor_at_offset_2.cmp(&anchor_at_offset_2, buffer)?,
Ordering::Equal
);
assert_eq!(
anchor_at_offset_0.cmp(&anchor_at_offset_1, buffer)?,
Ordering::Less
);
assert_eq!(
anchor_at_offset_1.cmp(&anchor_at_offset_2, buffer)?,
Ordering::Less
);
assert_eq!(
anchor_at_offset_0.cmp(&anchor_at_offset_2, buffer)?,
Ordering::Less
);
assert_eq!(
anchor_at_offset_1.cmp(&anchor_at_offset_0, buffer)?,
Ordering::Greater
);
assert_eq!(
anchor_at_offset_2.cmp(&anchor_at_offset_1, buffer)?,
Ordering::Greater
);
assert_eq!(
anchor_at_offset_2.cmp(&anchor_at_offset_0, buffer)?,
Ordering::Greater
);
Ok(())
})
})
}
#[test]
fn test_anchors_at_start_and_end() -> Result<()> {
App::test((), |mut app| async move {
let buffer_model: &mut ModelHandle<Buffer> = &mut app.add_model(|_| Buffer::new(""));
buffer_model.update(&mut app, |buffer, ctx| {
let before_start_anchor = buffer.anchor_before(0).unwrap();
let after_end_anchor = buffer.anchor_after(0).unwrap();
buffer.edit_for_test(
vec![to_char_index_range(0..0)],
"abc",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "abc");
assert_eq!(
before_start_anchor.to_char_offset(buffer).unwrap(),
0.into()
);
assert_eq!(after_end_anchor.to_char_offset(buffer).unwrap(), 3.into());
let after_start_anchor = buffer.anchor_after(0).unwrap();
let before_end_anchor = buffer.anchor_before(3).unwrap();
buffer.edit_for_test(
vec![to_char_index_range(3..3)],
"def",
EditOrigin::UserInitiated,
ctx,
)?;
buffer.edit_for_test(
vec![to_char_index_range(0..0)],
"ghi",
EditOrigin::UserInitiated,
ctx,
)?;
assert_eq!(buffer.text(), "ghiabcdef");
assert_eq!(
before_start_anchor.to_char_offset(buffer).unwrap(),
0.into()
);
assert_eq!(after_start_anchor.to_char_offset(buffer).unwrap(), 3.into());
assert_eq!(before_end_anchor.to_char_offset(buffer).unwrap(), 6.into());
assert_eq!(after_end_anchor.to_char_offset(buffer).unwrap(), 9.into());
Ok(())
})
})
}
#[test]
fn test_concurrent_insertions() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "quick".
// Concurrently, one replica adds "The " to the start
// while the other replica adds " brown" to the end.
let base_text = "quick";
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(1), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(2), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..0)],
"The ",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "The quick");
});
let buffer_1_edit = buffer_1_rx.next().await.unwrap().to_vec().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(5..5)],
" brown",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 2");
assert_eq!(buffer.text(), "quick brown");
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec().to_vec();
// Both replicas should converge to "The quick brown".
let expected_text = "The quick brown";
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply replica 2's edits to replica 1");
assert_eq!(buffer.text(), expected_text);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit, ctx)
.expect("can apply replica 1's edits to replica 2");
assert_eq!(buffer.text(), expected_text);
});
})
}
#[test]
fn test_concurrent_insertions_at_same_location() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "quick".
// Concurrently, one replica adds an "One" to the start
// while the other replica adds "The" to the start.
// We break the tie by using replica ID.
let base_text = "quick";
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(1), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(2), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..0)],
"One ",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "One quick");
});
let buffer_1_edit = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..0)],
"The ",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 2");
assert_eq!(buffer.text(), "The quick");
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec();
// The buffers should converge to "The One quick" because
// we break ties using (lamport, replica ID). In this case, both ops
// have equal lamport timestamps, and thus we break the tie using replica ID
// (replica 2 wins).
let expected_text = "The One quick";
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply replica 2's edits to replica 1");
assert_eq!(buffer.text(), expected_text);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit, ctx)
.expect("can apply replica 1's edits to replica 2");
assert_eq!(buffer.text(), expected_text);
});
// Replica 1 should still be able to edit at the start of the buffer.
// If we only relies on replica ID to break ties, replica 1 would never be able
// to insert at the start of the buffer. But in this case, replica 1's edit will have a larger
// lamport timestamp.
let expected_text = "One day The One quick";
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..0)],
"One day ",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), expected_text);
});
let buffer_1_edit = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit, ctx)
.expect("can apply replica 1's edits to replica 2");
assert_eq!(buffer.text(), expected_text);
});
})
}
#[test]
fn test_concurrent_non_overlapping_deletions() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "The quick brown jumps".
// Concurrently, one replica deletes " brown"
// while the other replica adds " fox" after "brown".
let base_text = "The quick brown jumps";
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(1), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(2), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(9..15)],
"",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "The quick jumps");
});
let buffer_1_edit = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(16..16)],
"fox ",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 2");
assert_eq!(buffer.text(), "The quick brown fox jumps");
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec();
// Although the inserted text was relative to the deleted text,
// the buffer should converge to "The quick fox jumps".
let expected_text = "The quick fox jumps";
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply replica 2's edits to replica 1");
assert_eq!(buffer.text(), expected_text);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit, ctx)
.expect("can apply replica 1's edits to replica 2");
assert_eq!(buffer.text(), expected_text);
});
})
}
#[test]
fn test_concurrent_overlapping_deletions() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "The quick brown jumps".
// Concurrently, one replica deletes "quick brown "
// while the other replica delete "brown ".
let base_text = "The quick brown jumps";
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(1), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(2), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(4..16)],
"",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "The jumps");
});
let buffer_1_edit = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(10..16)],
"",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 2");
assert_eq!(buffer.text(), "The quick jumps");
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec();
// Replica 2's deletion is a subset of replica 1's deletion,
// so the buffers should converge to "The jumps".
let expected_text = "The jumps";
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply replica 2's edits to replica 1");
assert_eq!(buffer.text(), expected_text);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit, ctx)
.expect("can apply replica 1's edits to replica 2");
assert_eq!(buffer.text(), expected_text);
});
})
}
#[test]
fn test_insertion_within_deletion() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "The quick brown jumps".
// Concurrently, one replica deletes " brown jumps"
// while the other replica inserts "fox" after "brown".
let base_text = "The quick brown jumps";
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(1), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(2), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(9..21)],
"",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "The quick");
});
let buffer_1_edit = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(16..16)],
"fox ",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 2");
assert_eq!(buffer.text(), "The quick brown fox jumps");
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec();
// Since replica 2's insertion was made when it saw the base text,
// it should be honored, so the buffers should converge to
// "The quickfox " (the space before the 'f' was deleted as part of replica 1's deletion).
let expected_text = "The quickfox ";
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply replica 2's edits to replica 1");
assert_eq!(buffer.text(), expected_text);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit, ctx)
.expect("can apply replica 1's edits to replica 2");
assert_eq!(buffer.text(), expected_text);
});
})
}
#[test]
fn test_concurrent_replace() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "The quick brown fox".
// Concurrently, one replica replaces "brown" with "red"
// while the other replica replaces "brown" with "white".
let base_text: &str = "The quick brown fox";
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(1), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(2), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(10..15)],
"red",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "The quick red fox");
});
let buffer_1_edit = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(10..15)],
"white",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 2");
assert_eq!(buffer.text(), "The quick white fox");
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec();
// The edits were concurrent, so we need to honor both.
// The buffer should converge to "The quick whitered fox".
// (Note: "white" before "red" because replica 2 > replica 1).
let expected_text = "The quick whitered fox";
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply replica 2's edits to replica 1");
assert_eq!(buffer.text(), expected_text);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit, ctx)
.expect("can apply replica 1's edits to replica 2");
assert_eq!(buffer.text(), expected_text);
});
})
}
#[test]
fn test_deferred_ops() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "abcd".
// Replica 1 makes 4 consecutive edits:
// 1. insert "1" before "a"
// 2. insert "2" before "b"
// 3. insert "3" before "c"
// 4. insert "4" before "d"
// Replica 2 gets the edits out of order ("2" -> "4" -> "1" -> "3").
// Since the edits are out of order, replica 2 needs to defer them until
// it's seen all previous edits (e.g. can't apply "2" until "1" is received).
// Despite this, replica 2 inserts "~" after "b" before any of replica 1's edits
// are actually applied.
let base_text = "abcd";
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(1), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(2), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..0)],
"1",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "1abcd");
});
let buffer_1_edit_1 = buffer_1_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(2..2)],
"2",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "1a2bcd");
});
let buffer_1_edit_2 = buffer_1_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(4..4)],
"3",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "1a2b3cd");
});
let buffer_1_edit_3 = buffer_1_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(6..6)],
"4",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "1a2b3c4d");
});
let buffer_1_edit_4 = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit_2, ctx)
.expect("can apply replica 1's edits to replica 2");
// Can't apply it so buffer text should not change.
assert_eq!(buffer.text(), "abcd");
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(2..2)],
"~",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 2");
assert_eq!(buffer.text(), "ab~cd");
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit_4, ctx)
.expect("can apply replica 1's edits to replica 2");
// Can't apply it so buffer text should not change.
assert_eq!(buffer.text(), "ab~cd");
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit_1, ctx)
.expect("can apply replica 1's edits to replica 2");
// Since we've received edit 1, we can also flush edit 2
// (but not edit 4 since edit 3 was not received).
assert_eq!(buffer.text(), "1a2b~cd");
});
let expected_text = "1a2b3~c4d";
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit_3, ctx)
.expect("can apply replica 1's edits to replica 2");
// Since we've received edit 3, we can also flush edit 4.
// Note: the '3' comes before '~' because the former edit had a larger
// lamport timestamp.
assert_eq!(buffer.text(), "1a2b3~c4d");
});
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply replica 2's edits to replica 1");
assert_eq!(buffer.text(), expected_text);
});
})
}
#[test]
fn test_duplicate_ops() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "The quick brown jumps".
// One replica adds "fox" before "jumps", but the edit is received
// twice by the second replica. The second replica should de-dupe
// and only apply it once.
let base_text = "The quick brown jumps";
let expected_text = "The quick brown fox jumps";
let (buffer_1, mut buffer_1_ops_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(1), base_text);
let (buffer_2, _) = new_buffer_with_ops_receiver(&mut app, ReplicaId::new(2), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(15..15)],
" fox",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), expected_text);
});
let buffer_1_edit = buffer_1_ops_rx.next().await.unwrap().to_vec();
let buffer_1_edit_clone = buffer_1_edit.clone();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit, ctx)
.expect("can apply replica 1's edits to replica 2");
assert_eq!(buffer.text(), expected_text);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit_clone, ctx)
.expect("can apply replica 1's edits to replica 2");
assert_eq!(buffer.text(), expected_text);
});
})
}
#[test]
fn test_multiple_peers() {
App::test((), |mut app| async move {
// This test was adapted from an occurrence of `test_random_concurrent_operations`.
// In this test, the buffer starts as "bar".
// We have three peers where each peer makes an edit.
let base_text = "bar";
let expected_text = "applebaz";
let (buffer_1, mut buffer_1_ops_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(1), base_text);
let (buffer_2, mut buffer_2_ops_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(2), base_text);
let (buffer_3, mut buffer_3_ops_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(3), base_text);
// Buffer 3 edits the base text.
buffer_3.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..0)],
"foo",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 3");
assert_eq!(buffer.text(), "foobar");
});
let buffer_3_edit_1 = buffer_3_ops_rx.next().await.unwrap().to_vec();
// Buffer 1 applies buffer 3's edit.
buffer_1.update(
&mut app,
enclose!((buffer_3_edit_1) move |buffer, ctx| {
buffer
.apply_ops(buffer_3_edit_1, ctx)
.expect("can apply replica 3's edits to replica 1");
assert_eq!(buffer.text(), "foobar");
}),
);
// Buffer 2 edits the base text.
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..0)],
"baz",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 2");
assert_eq!(buffer.text(), "bazbar");
});
let buffer_2_edit_1 = buffer_2_ops_rx.next().await.unwrap().to_vec();
// Buffer 3 applies buffer 2's edit.
buffer_3.update(
&mut app,
enclose!((buffer_2_edit_1) move|buffer, ctx| {
buffer
.apply_ops(buffer_2_edit_1, ctx)
.expect("can apply replica 2's edits to replica 3");
assert_eq!(buffer.text(), "foobazbar");
}),
);
// Buffer 1 edits their buffer (recall buffer 1 observed buffer 3's first edit).
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..6)],
"apple",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "apple");
});
let buffer_1_edit_1 = buffer_1_ops_rx.next().await.unwrap().to_vec();
// Buffer 3 applies buffer 1's edit.
buffer_3.update(
&mut app,
enclose!((buffer_1_edit_1) move |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit_1, ctx)
.expect("can apply replica 1's edits to replica 3");
assert_eq!(buffer.text(), expected_text);
}),
);
// Buffer 2 applies buffer 3's edit.
buffer_2.update(&mut app, move |buffer, ctx| {
buffer
.apply_ops(buffer_3_edit_1, ctx)
.expect("can apply replica 1's edits to replica 3");
assert_eq!(buffer.text(), "foobazbar");
});
// Buffer 2 applies buffer 1's edit.
buffer_2.update(&mut app, move |buffer, ctx| {
// Recall: buffer_1_edit_1 replaced "foobar" with "apple".
buffer
.apply_ops(buffer_1_edit_1, ctx)
.expect("can apply replica 1's edits to replica 3");
assert_eq!(buffer.text(), expected_text);
});
// Buffer 1 applies buffer 2's edit.
buffer_1.update(&mut app, move |buffer, ctx| {
// Recall: buffer_2_edit_1 added "baz" before "bar".
buffer
.apply_ops(buffer_2_edit_1, ctx)
.expect("can apply replica 1's edits to replica 3");
assert_eq!(buffer.text(), expected_text);
});
// At this point, all peers have applied each other's edits and converged to the same buffer.
})
}
#[test]
fn test_random_concurrent_operations() {
App::test((), |mut app| async move {
const PEERS: usize = 10;
for seed in 0..1000 {
println!("seed={seed}");
let mut rng = &mut StdRng::seed_from_u64(seed);
let base_text_len = rng.gen_range(0..10);
let base_text = RandomCharIter::new(&mut rng)
.take(base_text_len)
.collect::<String>();
let mut replica_ids = Vec::new();
let mut buffers = Vec::new();
let mut network = Network::new();
for i in 1..=PEERS {
let replica_id = ReplicaId::new(i);
let (buffer, ops_rx) =
new_buffer_with_ops_receiver(&mut app, replica_id.clone(), base_text.as_str());
buffers.push((buffer, ops_rx));
replica_ids.push(replica_id.clone());
network.add_peer(replica_id);
}
let mut mutation_count = 10;
let mut replicas_that_changed_selections = HashSet::new();
loop {
let replica_index = rng.gen_range(0..PEERS);
let replica_id = replica_ids[replica_index].clone();
let (buffer, ops_rx) = &buffers[replica_index];
let mut ops_rx = ops_rx.clone();
if mutation_count > 0 && rng.gen() {
let mutation_type = buffer.update(&mut app, |buffer, ctx| {
buffer.randomly_mutate(&mut rng, ctx)
});
if matches!(
mutation_type,
RandomMutationType::ChangeSelections { changed: true }
) {
replicas_that_changed_selections.insert(replica_id.clone());
}
let ops = ops_rx.next().await.unwrap().to_vec();
network.broadcast(replica_id, ops, &mut rng);
mutation_count -= 1;
} else if network.has_unreceived(&replica_id) {
let ops = network.receive(replica_id, &mut rng);
buffer.update(&mut app, |buffer, ctx| {
buffer.apply_ops(ops, ctx).unwrap();
});
}
if mutation_count == 0 && network.is_idle() {
break;
}
}
let (expected_text, _expected_selections) = buffers[0].0.read(&app, |buffer, _ctx| {
// Only include our own selections if they ever changed
// (otherwise, we wouldn't expect peers to have a selection state for us).
let all_selections = buffer.all_selections(
replicas_that_changed_selections.contains(&buffer.replica_id()),
);
(buffer.text(), all_selections)
});
for (buffer, _) in &buffers[1..] {
buffer.read(&app, |buffer, _ctx| {
assert_eq!(
buffer.text(),
expected_text,
"Replica {:?}'s text differs from replica 1",
buffer.lamport_clock.replica_id
);
// TODO(suraj): enable these assertions once
// we fixed selection consistency.
//
// // Only include our own selections if they ever changed
// // (otherwise, we wouldn't expect peers to have a selection state for us).
// let actual_selections = buffer.all_selections(dbg!(
// replicas_that_changed_selections.contains(&buffer.replica_id())
// ));
// assert_eq!(
// actual_selections, expected_selections,
// "Replica {:?}'s selections differs from replica 1",
// buffer.lamport_clock.replica_id
// );
});
}
}
})
}
#[test]
fn test_get_word_near_point() {
{
let buffer = Buffer::new("echo 'foo bar' text-data");
for col in 0..=3 {
assert_eq!(
buffer.get_word_nearest_to_point(&Point::new(0, col)),
Some("echo".to_owned())
);
}
for col in 4..=8 {
assert_eq!(
buffer.get_word_nearest_to_point(&Point::new(0, col)),
Some("foo".to_owned())
);
}
for col in 9..=16 {
assert_eq!(
buffer.get_word_nearest_to_point(&Point::new(0, col)),
Some("bar".to_owned())
);
}
for col in 17..=22 {
assert_eq!(
buffer.get_word_nearest_to_point(&Point::new(0, col)),
Some("text".to_owned())
);
}
for col in 23..=27 {
assert_eq!(
buffer.get_word_nearest_to_point(&Point::new(0, col)),
Some("data".to_owned())
);
}
}
{
let buffer = Buffer::new("");
assert_eq!(buffer.get_word_nearest_to_point(&Point::new(0, 0)), None);
}
{
let buffer = Buffer::new(" :) ");
assert_eq!(buffer.get_word_nearest_to_point(&Point::new(0, 2)), None);
}
{
let buffer = Buffer::new("foo\n \nbar");
assert_eq!(buffer.get_word_nearest_to_point(&Point::new(1, 1)), None);
assert_eq!(
buffer.get_word_nearest_to_point(&Point::new(2, 1)),
Some("bar".to_owned())
);
}
{
let buffer = Buffer::new("\necho foo\n");
assert_eq!(buffer.get_word_nearest_to_point(&Point::new(0, 0)), None);
assert_eq!(
buffer.get_word_nearest_to_point(&Point::new(1, 0)),
Some("echo".to_owned())
);
}
}
#[test]
fn test_undo_redo() -> Result<()> {
App::test((), |mut app| async move {
// In this test, we rely on the fact that [`Buffer::edit_for_test`] records
// edits with the [`Action::ReplaceBuffer`] action which is atomic.
// Therefore, each edit corresponds to one undo stack entry.
let buffer = app.add_model(|_| Buffer::new("hello"));
buffer.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "hello");
// Undo to start should be a no-op.
buffer.undo(ctx);
assert_eq!(buffer.text(), "hello");
// Same for redo.
buffer.redo(ctx);
assert_eq!(buffer.text(), "hello");
// Make an edit, undo it and then redo it.
buffer
.edit_for_test(
vec![to_char_index_range(5..5)],
" world",
EditOrigin::UserInitiated,
ctx,
)
.unwrap();
assert_eq!(buffer.text(), "hello world");
buffer.undo(ctx);
assert_eq!(buffer.text(), "hello");
buffer.redo(ctx);
assert_eq!(buffer.text(), "hello world");
// Make two consecutive edits (i.e. two undo stack entries).
buffer
.edit_for_test(
vec![to_char_index_range(5..5)],
"?",
EditOrigin::UserInitiated,
ctx,
)
.unwrap();
assert_eq!(buffer.text(), "hello? world");
buffer
.edit_for_test(
vec![to_char_index_range(0..1)],
"j",
EditOrigin::UserInitiated,
ctx,
)
.unwrap();
assert_eq!(buffer.text(), "jello? world");
// Undo both edits.
buffer.undo(ctx);
assert_eq!(buffer.text(), "hello? world");
buffer.undo(ctx);
assert_eq!(buffer.text(), "hello world");
// Redo the old edit.
buffer.redo(ctx);
assert_eq!(buffer.text(), "hello? world");
// Make an edit while we're not at the top of the
// undo stack (because there were two undos followed by
// only one redo).
buffer
.edit_for_test(
vec![to_char_index_range(0..5)],
"bye",
EditOrigin::UserInitiated,
ctx,
)
.unwrap();
assert_eq!(buffer.text(), "bye? world");
// Since we edited while we were not at the top of the undo stack,
// the stack should be truncated and redo should be a no-op.
buffer.redo(ctx);
assert_eq!(buffer.text(), "bye? world");
buffer.undo(ctx);
assert_eq!(buffer.text(), "hello? world");
});
Ok(())
})
}
#[test]
fn test_only_one_undo_record_per_batch() -> Result<()> {
App::test((), |mut app| async move {
let buffer = app.add_model(|_| Buffer::new("hello"));
buffer.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "hello");
buffer.start_edits_and_selection_changes_batch(
EditOrigin::UserInitiated,
PlainTextEditorViewAction::ReplaceBuffer,
false,
);
buffer
.edit(vec![to_char_index_range(5..5)], " world", ctx)
.unwrap();
assert_eq!(buffer.text(), "hello world");
buffer
.edit(vec![to_char_index_range(11..11)], "!", ctx)
.unwrap();
assert_eq!(buffer.text(), "hello world!");
buffer.end_batch(ctx);
});
// The edits should be coalesced as one entry on the undo stack.
buffer.update(&mut app, |buffer, ctx| {
buffer.undo(ctx);
assert_eq!(buffer.text(), "hello");
});
Ok(())
})
}
#[test]
fn test_record_edits() -> Result<()> {
App::test((), |mut app| async move {
let buffer = app.add_model(|_| Buffer::new("hello"));
buffer.update(&mut app, |buffer, ctx| {
assert_eq!(buffer.text(), "hello");
buffer.start_edits_and_selection_changes_batch(
EditOrigin::UserInitiated,
PlainTextEditorViewAction::ReplaceBuffer,
false,
);
buffer
.edit(vec![to_char_index_range(5..5)], " world", ctx)
.unwrap();
assert_eq!(buffer.text(), "hello world");
// Force a record.
buffer.record_edits(PlainTextEditorViewAction::ReplaceBuffer, ctx);
buffer
.edit(vec![to_char_index_range(11..11)], "!", ctx)
.unwrap();
assert_eq!(buffer.text(), "hello world!");
buffer.end_batch(ctx);
});
// The edits should be two separate entries on the undo stack.
buffer.update(&mut app, |buffer, ctx| {
buffer.undo(ctx);
assert_eq!(buffer.text(), "hello world");
buffer.redo(ctx);
assert_eq!(buffer.text(), "hello world!");
buffer.undo(ctx);
buffer.undo(ctx);
assert_eq!(buffer.text(), "hello");
});
Ok(())
})
}
#[test]
fn test_remote_undo_redo() {
App::test((), |mut app| async move {
// In this test, the buffer begins as empty.
// One replica makes three insertions: "a", "b" and "c".
// Upon receiving these edits, another replica changes "b" -> "d".
// When replica one performs undo, it can only undo its own operations,
// while respecting any changes made by peers.
let base_text: &str = "";
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(1), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(2), base_text);
// Set up the edits.
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..0)],
"a",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "a");
});
let buffer_1_edit_1 = buffer_1_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(1..1)],
"b",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "ab");
});
let buffer_1_edit_2 = buffer_1_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(2..2)],
"c",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "abc");
});
let buffer_1_edit_3 = buffer_1_rx.next().await.unwrap().to_vec();
// Make sure replica 2 acknowledges the edits.
let ops: Vec<Operation> = buffer_1_edit_1
.into_iter()
.chain(buffer_1_edit_2)
.chain(buffer_1_edit_3)
.collect();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(ops, ctx)
.expect("can apply replica 1's edits to replica 2");
assert_eq!(buffer.text(), "abc");
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(1..2)],
"d",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 2");
assert_eq!(buffer.text(), "adc");
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec();
// Undo!
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply replica 2's edits to replica 1");
assert_eq!(buffer.text(), "adc");
});
buffer_1.update(&mut app, |buffer, ctx| {
// The first undo should only undo buffer 1's local edit (not buffer 2's latest edit).
buffer.undo(ctx);
assert_eq!(buffer.text(), "ad");
});
let buffer_1_undo_1 = buffer_1_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
// The second undo should be a no-op because the "b" edit is no longer visible anyways.
buffer.undo(ctx);
assert_eq!(buffer.text(), "ad");
});
let buffer_1_undo_2 = buffer_1_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer.undo(ctx);
assert_eq!(buffer.text(), "d");
});
let buffer_1_undo_3 = buffer_1_rx.next().await.unwrap().to_vec();
// Apply undo's to replica 2.
let ops: Vec<Operation> = buffer_1_undo_1
.into_iter()
.chain(buffer_1_undo_2)
.chain(buffer_1_undo_3)
.collect();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(ops, ctx)
.expect("can apply replica 1's undos to replica 2");
assert_eq!(buffer.text(), "d");
});
// Redo!
buffer_1.update(&mut app, |buffer, ctx| {
// The first redo should bring back the 'a'.
buffer.redo(ctx);
assert_eq!(buffer.text(), "ad");
});
let buffer_1_redo_1 = buffer_1_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
// The second redo should be a no-op because the "d" edit is still present.
buffer.redo(ctx);
assert_eq!(buffer.text(), "ad");
});
let buffer_1_redo_2 = buffer_1_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
// The third redo should bring back the 'c'.
buffer.redo(ctx);
assert_eq!(buffer.text(), "adc");
});
let buffer_1_redo_3 = buffer_1_rx.next().await.unwrap().to_vec();
// Apply redo's to replica 2!
let ops: Vec<Operation> = buffer_1_redo_1
.into_iter()
.chain(buffer_1_redo_2)
.chain(buffer_1_redo_3)
.collect();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(ops, ctx)
.expect("can apply replica 1's redos to replica 2");
assert_eq!(buffer.text(), "adc");
});
})
}
#[test]
fn test_concurrent_edit_with_undo() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "foobar".
// Replica 1 adds '!' between 'foo' and 'bar' and replica 2 receives this.
// Then, concurrently, replica 1 does an undo while replica 2
// removes "foo".
let base_text: &str = "foobar";
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(1), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(2), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(3..3)],
"!",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "foo!bar");
});
let buffer_1_edit = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit, ctx)
.expect("can apply replica 1's edits to replica 2");
assert_eq!(buffer.text(), "foo!bar");
});
buffer_1.update(&mut app, |buffer, ctx| {
buffer.undo(ctx);
assert_eq!(buffer.text(), "foobar");
});
let buffer_1_undo = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..3)],
"",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 2");
assert_eq!(buffer.text(), "!bar");
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply replica 2's edits to replica 1");
assert_eq!(buffer.text(), "bar");
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_undo, ctx)
.expect("can apply replica 1's undo to replica 2");
assert_eq!(buffer.text(), "bar");
});
})
}
#[test]
fn test_concurrent_edit_with_conflicting_undo() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "foobar".
// Replica 1 adds 'baz' between 'foo' and 'bar' and replica 2 receives this.
// Then, concurrently, replica 1 does an undo while replica 2
// changes the 'z' to 'x'.
let base_text: &str = "foobar";
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(1), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(2), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(3..3)],
"baz",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "foobazbar");
});
let buffer_1_edit = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit, ctx)
.expect("can apply replica 1's edits to replica 2");
assert_eq!(buffer.text(), "foobazbar");
});
buffer_1.update(&mut app, |buffer, ctx| {
buffer.undo(ctx);
assert_eq!(buffer.text(), "foobar");
});
let buffer_1_undo = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(5..6)],
"x",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 2");
assert_eq!(buffer.text(), "foobaxbar");
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply replica 2's edits to replica 1");
assert_eq!(buffer.text(), "fooxbar");
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_undo, ctx)
.expect("can apply replica 1's undo to replica 2");
assert_eq!(buffer.text(), "fooxbar");
});
})
}
#[test]
fn test_deletions_with_undo() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "abc".
// Replica 1 changes 'a' to '1'.
// Upon receiving replica 1's edit, replica 2 replaces "1bc" with "0".
// Upon receiving replica 2's edit, replica 1 performs an undo.
// Replica 1's undo should be a no-op because their edit is not visible anymore.
// The final text should be "0".
let base_text: &str = "abc";
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(1), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(2), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..1)],
"1",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "1bc");
});
let buffer_1_edit = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit, ctx)
.expect("can apply replica 1's edits to replica 2");
assert_eq!(buffer.text(), "1bc");
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..3)],
"0",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 2");
assert_eq!(buffer.text(), "0");
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply replica 2's edits to replica 1");
assert_eq!(buffer.text(), "0");
});
buffer_1.update(&mut app, |buffer, ctx| {
buffer.undo(ctx);
assert_eq!(buffer.text(), "0");
});
let buffer_1_undo = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_undo, ctx)
.expect("can apply replica 1's undo to replica 2");
assert_eq!(buffer.text(), "0");
});
})
}
#[test]
fn test_concurrent_deletions_with_undo() {
App::test((), |mut app| async move {
// This test is similar to [`test_deletions_with_undo`] but
// the edits here happen concurrently rather than sequentially.
//
// In this test, the buffer begins as "abc".
// Concurrently:
// - Replica 1 changes 'a' to '1'.
// - Replica 2 changes "abc" to "0".
// The expected text as this point should be "01" because
// the deletion of "a" was made concurrent to the edit.
//
// Now, suppose replica 1 performs an undo.
// The expected text should be "0" because while we
// are reverting "1" -> "a", the edit for "a" has been deleted
// by replica 2 (which has not been undone).
//
// If replica 2 undoes at this point, then the
// buffer text should be "abc" (not "1bc" because
// replica 1 already undid).
let base_text: &str = "abc";
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(1), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(2), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..1)],
"1",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "1bc");
});
let buffer_1_edit = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..3)],
"0",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 2");
assert_eq!(buffer.text(), "0");
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply replica 2's edits to replica 1");
assert_eq!(buffer.text(), "01");
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit, ctx)
.expect("can apply replica 1's edits to replica 2");
assert_eq!(buffer.text(), "01");
});
buffer_1.update(&mut app, |buffer, ctx| {
buffer.undo(ctx);
assert_eq!(buffer.text(), "0");
});
let buffer_1_undo = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_undo, ctx)
.expect("can apply replica 1's undo to replica 2");
assert_eq!(buffer.text(), "0");
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer.undo(ctx);
assert_eq!(buffer.text(), "abc");
});
let buffer_2_undo = buffer_2_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_undo, ctx)
.expect("can apply replica 2's undo to replica 1");
assert_eq!(buffer.text(), "abc");
});
})
}
#[test]
fn test_out_of_order_undo() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "abc".
// Replica 1 makes the following consecutive changes:
// - replace 'a' with 1
// - undo
// - replace 'c' to '3'
// which produces the text 'ab2'.
//
// Suppose replica 2 receives these in reverse order.
// Operations should be deferred and the final text should be the same.
let base_text: &str = "abc";
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(1), base_text);
let (buffer_2, _buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, ReplicaId::new(2), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..1)],
"1",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "1bc");
});
let buffer_1_edit_1 = buffer_1_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer.undo(ctx);
assert_eq!(buffer.text(), "abc");
});
let buffer_1_undo = buffer_1_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(2..3)],
"3",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit buffer 1");
assert_eq!(buffer.text(), "ab3");
});
let buffer_1_edit_2 = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit_2, ctx)
.expect("can apply replica the last edit");
assert_eq!(buffer.text(), "abc");
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_undo, ctx)
.expect("can apply replica the last edit");
assert_eq!(buffer.text(), "abc");
});
buffer_2.update(&mut app, |buffer, ctx| {
// Once the first edit is applied, all ops should be flushed.
buffer
.apply_ops(buffer_1_edit_1, ctx)
.expect("can apply replica the last edit");
assert_eq!(buffer.text(), "ab3");
});
})
}
#[test]
fn test_stable_selections() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "abc".
// Replica 1 moves their cursor to after 'b'.
// Replica 2 makes edits before and after 'b'.
// Replica 1's cursor should stay stable (not move).
let base_text: &str = "abc";
let (replica_1_id, replica_2_id) = (ReplicaId::new(1), ReplicaId::new(2));
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, replica_1_id.clone(), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, replica_2_id.clone(), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.change_selections_for_test(vec![to_char_index_range(2..2)], ctx)
.expect("can change selections for replica 1");
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(2..2)]
);
});
let buffer_1_selection_update = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_selection_update, ctx)
.expect("can apply selection change to replica 2");
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(2..2)]
);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..0), to_char_index_range(2..2)],
"1",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit replica 2");
assert_eq!(buffer.text(), "1ab1c");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(3..3)]
);
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply edit to replica 1");
assert_eq!(buffer.text(), "1ab1c");
assert_eq!(
buffer.selections_for_replica(replica_1_id),
vec![to_char_index_range(3..3)]
);
});
})
}
#[test]
fn test_replace_selected_text() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "abc".
// Replica 1 selects the text 'b'.
// Replica 2 then removes the 'b'.
// Replica 1's selections should collapsed to a cursor because the
// edit that the selection was based on is no longer visible.
// If replica 2 adds back the 'b', replica 1's selections should be unchanged.
let base_text: &str = "abc";
let (replica_1_id, replica_2_id) = (ReplicaId::new(1), ReplicaId::new(2));
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, replica_1_id.clone(), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, replica_2_id.clone(), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.change_selections_for_test(vec![to_char_index_range(1..2)], ctx)
.expect("can change selections for replica 1");
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(1..2)]
);
});
let buffer_1_selection_update = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_selection_update, ctx)
.expect("can apply selection change to replica 2");
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(1..2)]
);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(1..2)],
"",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit replica 2");
assert_eq!(buffer.text(), "ac");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(1..1)],
);
});
let buffer_2_edit_1 = buffer_2_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit_1, ctx)
.expect("can apply edit to replica 1");
assert_eq!(buffer.text(), "ac");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(1..1)],
);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(1..1)],
"b",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit replica 2");
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(1..1)],
);
});
let buffer_2_edit_2 = buffer_2_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit_2, ctx)
.expect("can apply edit to replica 1");
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id),
vec![to_char_index_range(1..1)],
);
});
})
}
#[test]
fn test_receiving_selection_change_before_edit() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "abc".
// Replica 1 adds '123' after 'a' and then selects it.
// Replica 2 receives the selection change before the edit operation.
// The selection change should be deferred until the edit is applied.
let base_text: &str = "abc";
let (replica_1_id, replica_2_id) = (ReplicaId::new(1), ReplicaId::new(2));
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, replica_1_id.clone(), base_text);
let (buffer_2, _buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, replica_2_id.clone(), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(1..1)],
"123",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit replica 1");
assert_eq!(buffer.text(), "a123bc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..0)]
);
});
let buffer_1_edit = buffer_1_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.change_selections_for_test(vec![to_char_index_range(1..4)], ctx)
.expect("can change selections for replica 1");
assert_eq!(buffer.text(), "a123bc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(1..4)]
);
});
let buffer_1_selection_update = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_selection_update, ctx)
.expect("can apply selection change to replica 2");
assert_eq!(buffer.text(), "abc");
assert!(buffer
.selections_for_replica(replica_1_id.clone())
.is_empty(),);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_edit, ctx)
.expect("can apply edit to replica 2");
assert_eq!(buffer.text(), "a123bc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(1..4)]
);
});
})
}
#[test]
fn test_receiving_out_of_date_selection_updates() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "abc".
// Replica 1 selects 'a'. And then replica 1 changes selections to 'c'.
// Suppose replica 2 receives the second update first.
// When it receives the first selection update, it should ignore it.
let base_text: &str = "abc";
let (replica_1_id, replica_2_id) = (ReplicaId::new(1), ReplicaId::new(2));
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, replica_1_id.clone(), base_text);
let (buffer_2, _buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, replica_2_id.clone(), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.change_selections_for_test(vec![to_char_index_range(0..1)], ctx)
.expect("can change selections for replica 1");
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..1)]
);
});
let buffer_1_selection_update_1 = buffer_1_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.change_selections_for_test(vec![to_char_index_range(2..3)], ctx)
.expect("can change selections for replica 1");
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(2..3)]
);
});
let buffer_1_selection_update_2 = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_selection_update_2, ctx)
.expect("can apply selection change to replica 2");
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(2..3)]
);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_selection_update_1, ctx)
.expect("can apply selection change to replica 2");
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(2..3)]
);
});
})
}
#[test]
fn test_undo_should_restore_selection() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "abc".
// Replica 1 selects 'a' and then removes it.
// If replica 1 undoes that action, the selection should be restored.
let base_text: &str = "abc";
let (replica_1_id, replica_2_id) = (ReplicaId::new(1), ReplicaId::new(2));
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, replica_1_id.clone(), base_text);
let (buffer_2, _buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, replica_2_id.clone(), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.change_selections_for_test(vec![to_char_index_range(0..1)], ctx)
.expect("can change selections for replica 1");
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..1)]
);
});
let buffer_1_selection_update = buffer_1_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..1)],
"",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit replica 1");
assert_eq!(buffer.text(), "bc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..0)]
);
});
let buffer_1_edit = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_selection_update, ctx)
.expect("can apply selection change to replica 2");
buffer
.apply_ops(buffer_1_edit, ctx)
.expect("can apply edit to replica 2");
assert_eq!(buffer.text(), "bc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..0)]
);
});
buffer_1.update(&mut app, |buffer, ctx| {
buffer.undo(ctx);
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..1)]
);
});
let buffer_1_undo = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_undo, ctx)
.expect("can apply undo to replica 2");
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..1)]
);
});
})
}
#[ignore = "The underlying issue here is the same as the one in test_merging_selections_with_concurrent_edits."]
#[test]
fn test_undo_should_not_restore_remote_selection() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "abc".
// Replica 1 selects 'a'.
// Replica 2 then removes it.
// Replica 2 then undoes its operation.
// Replica 1's selection should _not_ be restored.
let base_text: &str = "abc";
let (replica_1_id, replica_2_id) = (ReplicaId::new(1), ReplicaId::new(2));
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, replica_1_id.clone(), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, replica_2_id.clone(), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.change_selections_for_test(vec![to_char_index_range(0..1)], ctx)
.expect("can change selections for replica 1");
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..1)]
);
});
let buffer_1_selection_update = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_selection_update, ctx)
.expect("can apply selection change to replica 2");
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..1)]
);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(0..1)],
"",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit replica 2");
assert_eq!(buffer.text(), "bc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..0)]
);
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply edit to replica 1");
assert_eq!(buffer.text(), "bc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..0)]
);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer.undo(ctx);
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..0)]
);
});
let buffer_2_undo = buffer_2_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_undo, ctx)
.expect("can apply undo to replica 1");
assert_eq!(buffer.text(), "abc");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..0)]
);
});
})
}
#[test]
fn test_merging_selections_with_remote_edits() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "abcdef".
// Replica 1 selects two ranges: "a" and "cdef".
// Then, replica 2 removes the 'b'.
// Every replica should merge the selection ranges.
let base_text: &str = "abcdef";
let (replica_1_id, replica_2_id, replica_3_id) =
(ReplicaId::new(1), ReplicaId::new(2), ReplicaId::new(3));
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, replica_1_id.clone(), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, replica_2_id.clone(), base_text);
let (buffer_3, _buffer_3_rx) =
new_buffer_with_ops_receiver(&mut app, replica_3_id.clone(), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.change_selections_for_test(
vec![to_char_index_range(0..1), to_char_index_range(2..6)],
ctx,
)
.expect("can change selections for replica 1");
assert_eq!(buffer.text(), "abcdef");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..1), to_char_index_range(2..6)]
);
});
let buffer_1_selection_update = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_selection_update.clone(), ctx)
.expect("can apply selection change to replica 2");
assert_eq!(buffer.text(), "abcdef");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..1), to_char_index_range(2..6)]
);
});
buffer_3.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_selection_update, ctx)
.expect("can apply selection change to replica 3");
assert_eq!(buffer.text(), "abcdef");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..1), to_char_index_range(2..6)]
);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(1..2)],
"",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit replica 2");
assert_eq!(buffer.text(), "acdef");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..5)],
);
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec();
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit.clone(), ctx)
.expect("can apply buffer 2's edit to buffer 1");
assert_eq!(buffer.text(), "acdef");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..5)],
);
});
buffer_3.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply buffer 2's edit to buffer 3");
assert_eq!(buffer.text(), "acdef");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..5)]
);
});
})
}
// See https://github.com/warpdotdev/warp-internal/pull/9249 for discussion
// about possible strategies to address this.
#[ignore = "The test points out an eventual consistency problem with selections."]
#[test]
fn test_merging_selections_with_concurrent_edits() {
App::test((), |mut app| async move {
// In this test, the buffer begins as "abcdef".
// Replica 1 selects two ranges: "a" and "cdef".
// Then, we have two concurrent edits:
// - Replica 2 removes the 'b'
// - Replica 3 inserts '1' after 'a'
//
// When replica 2 removes the 'b', replica 1's selection
// range should be collapsed into one. When replica 2 processes
// replica 3's edit, the new character would be part of the selected range.
//
// When replica 3 inesrts the '1' after 'a', replica 1's selections
// are still split in two ranges. When replica 3 processes replica 2's edit,
// the selections don't collapse.
//
// Now, replica 2 and replica 3 have different interpretations of replica 1's selection state.
//
// Suppose replica 1 processes replica 2's edit before replica 3's edit.
// In this case, when we process replica 2's edit, we would collapse
// the selection ranges into one and then when inserting replica 3's edit,
// it would be part of the selected range. So Replica 1 would expect
// "a1cdef" where the entire range is selected.
//
// We need to get replica 3 to be consistent now.
let base_text: &str = "abcdef";
let (replica_1_id, replica_2_id, replica_3_id) =
(ReplicaId::new(1), ReplicaId::new(2), ReplicaId::new(3));
let (buffer_1, mut buffer_1_rx) =
new_buffer_with_ops_receiver(&mut app, replica_1_id.clone(), base_text);
let (buffer_2, mut buffer_2_rx) =
new_buffer_with_ops_receiver(&mut app, replica_2_id.clone(), base_text);
let (buffer_3, mut buffer_3_rx) =
new_buffer_with_ops_receiver(&mut app, replica_3_id.clone(), base_text);
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.change_selections_for_test(
vec![to_char_index_range(0..1), to_char_index_range(2..6)],
ctx,
)
.expect("can change selections for replica 1");
assert_eq!(buffer.text(), "abcdef");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..1), to_char_index_range(2..6)]
);
});
let buffer_1_selection_update = buffer_1_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_selection_update.clone(), ctx)
.expect("can apply selection change to replica 2");
assert_eq!(buffer.text(), "abcdef");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..1), to_char_index_range(2..6)]
);
});
buffer_3.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_1_selection_update, ctx)
.expect("can apply selection change to replica 3");
assert_eq!(buffer.text(), "abcdef");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..1), to_char_index_range(2..6)]
);
});
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(1..2)],
"",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit replica 2");
assert_eq!(buffer.text(), "acdef");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..5)],
);
});
let buffer_2_edit = buffer_2_rx.next().await.unwrap().to_vec();
buffer_3.update(&mut app, |buffer, ctx| {
buffer
.edit_for_test(
vec![to_char_index_range(1..1)],
"1",
EditOrigin::UserInitiated,
ctx,
)
.expect("can edit replica 3");
assert_eq!(buffer.text(), "a1bcdef");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..1), to_char_index_range(3..7)],
);
});
let buffer_3_edit = buffer_3_rx.next().await.unwrap().to_vec();
buffer_2.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_3_edit.clone(), ctx)
.expect("can apply buffer 3's edit to buffer 2");
assert_eq!(buffer.text(), "a1cdef");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..6)]
);
});
buffer_3.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit.clone(), ctx)
.expect("can apply buffer 2's edit to buffer 3");
assert_eq!(buffer.text(), "a1cdef");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..1), to_char_index_range(2..6)]
);
});
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_2_edit, ctx)
.expect("can apply buffer 2's edit to buffer 1");
assert_eq!(buffer.text(), "acdef");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..5)],
);
});
buffer_1.update(&mut app, |buffer, ctx| {
buffer
.apply_ops(buffer_3_edit, ctx)
.expect("can apply buffer 3's edit to buffer 1");
assert_eq!(buffer.text(), "a1cdef");
assert_eq!(
buffer.selections_for_replica(replica_1_id.clone()),
vec![to_char_index_range(0..6)],
);
});
})
}
/// The type of random mutation [`Buffer::randomly_mutate`] applied.
pub enum RandomMutationType {
Edit,
ChangeSelections {
/// True iff the selections before
/// the change are not equal to the
/// selections after.
changed: bool,
},
Undo,
Redo,
}
impl Buffer {
pub fn randomly_edit<T>(
&mut self,
rng: &mut T,
ranges: RangesWhenEditing,
ctx: &mut ModelContext<Self>,
) -> (Vec<Range<CharOffset>>, String)
where
T: Rng,
{
let ranges = match ranges {
RangesWhenEditing::UseExistingSelections => self
.local_selections
.selections
.iter()
.map(|selection| {
selection.start().to_char_offset(self).unwrap()
..selection.end().to_char_offset(self).unwrap()
})
.collect(),
RangesWhenEditing::UseRandomRanges { num_ranges } => {
random_ranges(rng, num_ranges, self.len().as_usize())
}
};
let new_text_len = rng.gen_range(0..10);
let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect();
self.edit_for_test(
ranges.clone(),
new_text.as_str(),
EditOrigin::UserInitiated,
ctx,
)
.unwrap();
(ranges, new_text)
}
pub fn randomly_change_selections<T>(
&mut self,
rng: &mut T,
ctx: &mut ModelContext<Self>,
) -> bool
where
T: Rng,
{
// Select between 1 to 3 ranges.
let max_num_ranges = rng.gen_range(1..=3);
let text_len = self.len();
let new_ranges = if text_len.as_usize() == 0 {
vec![CharOffset::zero()..CharOffset::zero()]
} else {
random_ranges(rng, max_num_ranges, text_len.as_usize())
};
self.change_selections_for_test(new_ranges, ctx).unwrap()
}
/// Performs one mutation of the buffer which should
/// produce one [`Event::UpdatePeers`] event.
pub fn randomly_mutate<T>(
&mut self,
rng: &mut T,
ctx: &mut ModelContext<Self>,
) -> RandomMutationType
where
T: Rng,
{
// Randomly mutate the buffer by editing, undo'ing, redo'ing, or changing selections.
// 70% of the time we edit.
// 10% of the time we update selections.
// 10% of the time we undo.
// 10% of the time we redo.
match rng.gen_range(0..10) {
0..=6 => {
let num_ranges = rng.gen_range(0..=5);
let ranges = if num_ranges == 0 {
RangesWhenEditing::UseExistingSelections
} else {
RangesWhenEditing::UseRandomRanges { num_ranges }
};
self.randomly_edit(rng, ranges, ctx);
RandomMutationType::Edit
}
7 => {
let changed = self.randomly_change_selections(rng, ctx);
RandomMutationType::ChangeSelections { changed }
}
8 => {
self.undo(ctx);
RandomMutationType::Undo
}
9 => {
self.redo(ctx);
RandomMutationType::Redo
}
_ => unreachable!(),
}
}
}
/// Randomly creates offset-based ranges (up to [`max_num_ranges`])
/// for a string of length `text_len`. The ranges are disjoint
/// and ordered by the start point.
fn random_ranges<T>(rng: &mut T, max_num_ranges: usize, text_len: usize) -> Vec<Range<CharOffset>>
where
T: Rng,
{
let mut new_ranges: Vec<Range<CharOffset>> = Vec::new();
let mut start = 0;
while new_ranges.len() < max_num_ranges && start <= text_len {
let new_start = rng.gen_range(start..=text_len);
let new_end = rng.gen_range(new_start..=text_len);
new_ranges.push(new_start.into()..new_end.into());
start = new_end + 1;
}
new_ranges
}