// 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> { buffer .fragments .items() .into_iter() .filter_map(|fragment| { fragment .is_visible(&buffer.undo_history) .then_some(fragment.text.text_style) }) .collect() } type OpsReceiver = Pin>>>>; /// 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, 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 { Ok(CharOffset::from(*self)) } } fn to_char_index_range(range: Range) -> Range { 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 = &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![ 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 = &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![ 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![ 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 = &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![ 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 = &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![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 = &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![ 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![ 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![ 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 = &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![ 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 = &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![ 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 = &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![ 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![ // "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 = &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![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 = &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![ 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![ 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![ 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![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 = &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 = &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 = &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 = &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 = &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::(); 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 = &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 = &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::(), "abcd\nefgh\nijkl\nmno\nPQrs"); let chars = buffer.chars_at(Point::new(1, 0))?; assert_eq!(chars.collect::(), "efgh\nijkl\nmno\nPQrs"); let chars = buffer.chars_at(Point::new(2, 0))?; assert_eq!(chars.collect::(), "ijkl\nmno\nPQrs"); let chars = buffer.chars_at(Point::new(3, 0))?; assert_eq!(chars.collect::(), "mno\nPQrs"); let chars = buffer.chars_at(Point::new(4, 0))?; assert_eq!(chars.collect::(), "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::(), " \"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 = &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 = &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::(); 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 = 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 = 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 = 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( &mut self, rng: &mut T, ranges: RangesWhenEditing, ctx: &mut ModelContext, ) -> (Vec>, 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( &mut self, rng: &mut T, ctx: &mut ModelContext, ) -> 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( &mut self, rng: &mut T, ctx: &mut ModelContext, ) -> 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(rng: &mut T, max_num_ranges: usize, text_len: usize) -> Vec> where T: Rng, { let mut new_ranges: Vec> = 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 }