From c7d97adf2382c0af0d0fa892f50206ea34e1fb1b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 12 May 2021 17:05:03 +0200 Subject: [PATCH 1/7] Introduce a `gpui::test` macro --- Cargo.lock | 9 +++++++++ Cargo.toml | 2 +- gpui/Cargo.toml | 1 + gpui/src/lib.rs | 1 + gpui_macros/Cargo.toml | 11 +++++++++++ gpui_macros/src/lib.rs | 39 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 gpui_macros/Cargo.toml create mode 100644 gpui_macros/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 1fba36fd12..ba63f85d85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1180,6 +1180,7 @@ dependencies = [ "etagere", "font-kit", "foreign-types", + "gpui_macros", "log", "metal", "num_cpus", @@ -1205,6 +1206,14 @@ dependencies = [ "usvg", ] +[[package]] +name = "gpui_macros" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "hashbrown" version = "0.9.1" diff --git a/Cargo.toml b/Cargo.toml index bf2d5c8143..8109db121b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["zed", "gpui", "fsevent", "scoped_pool"] +members = ["zed", "gpui", "gpui_macros", "fsevent", "scoped_pool"] [patch.crates-io] async-task = {git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e"} diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 8c7c3bf4cb..d421a5a2f3 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -8,6 +8,7 @@ version = "0.1.0" async-task = "4.0.3" ctor = "0.1" etagere = "0.2" +gpui_macros = {path = "../gpui_macros"} log = "0.4" num_cpus = "1.13" ordered-float = "2.1.1" diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index edf14bc65c..19aac02c40 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -24,6 +24,7 @@ pub mod color; pub mod json; pub mod keymap; mod platform; +pub use gpui_macros::test; pub use platform::{Event, PathPromptOptions}; pub use presenter::{ AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext, diff --git a/gpui_macros/Cargo.toml b/gpui_macros/Cargo.toml new file mode 100644 index 0000000000..19a8e11eb6 --- /dev/null +++ b/gpui_macros/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "gpui_macros" +version = "0.1.0" +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +syn = "1.0" +quote = "1.0" diff --git a/gpui_macros/src/lib.rs b/gpui_macros/src/lib.rs new file mode 100644 index 0000000000..1045868777 --- /dev/null +++ b/gpui_macros/src/lib.rs @@ -0,0 +1,39 @@ +use std::mem; + +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, ItemFn}; + +#[proc_macro_attribute] +pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { + assert!(args.is_empty()); + + let mut inner_fn = parse_macro_input!(function as ItemFn); + let inner_fn_name = format_ident!("_{}", inner_fn.sig.ident); + let outer_fn_name = mem::replace(&mut inner_fn.sig.ident, inner_fn_name.clone()); + let outer_fn = if inner_fn.sig.asyncness.is_some() { + quote! { + #[test] + fn #outer_fn_name() { + #inner_fn + + gpui::App::test_async((), move |ctx| async { + #inner_fn_name(ctx).await; + }); + } + } + } else { + quote! { + #[test] + fn #outer_fn_name() { + #inner_fn + + gpui::App::test((), |ctx| { + #inner_fn_name(ctx); + }); + } + } + }; + + TokenStream::from(outer_fn) +} From 1c2b381f741abd35fbea155d68dc53817e9298d1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 12 May 2021 17:18:12 +0200 Subject: [PATCH 2/7] Use `gpui::test` macro for synchronous tests in zed --- zed/src/editor/buffer/mod.rs | 1065 +++++----- zed/src/editor/buffer_view.rs | 2485 ++++++++++++------------ zed/src/editor/display_map/fold_map.rs | 651 +++---- zed/src/editor/display_map/mod.rs | 99 +- zed/src/workspace.rs | 106 +- 5 files changed, 2141 insertions(+), 2265 deletions(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index d42c3460cf..058fb03806 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -2393,259 +2393,245 @@ mod tests { sync::atomic::{self, AtomicUsize}, }; - #[test] - fn test_edit() { - App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "abc", ctx); - assert_eq!(buffer.text(), "abc"); - buffer.edit(vec![3..3], "def", None).unwrap(); - assert_eq!(buffer.text(), "abcdef"); - buffer.edit(vec![0..0], "ghi", None).unwrap(); - assert_eq!(buffer.text(), "ghiabcdef"); - buffer.edit(vec![5..5], "jkl", None).unwrap(); - assert_eq!(buffer.text(), "ghiabjklcdef"); - buffer.edit(vec![6..7], "", None).unwrap(); - assert_eq!(buffer.text(), "ghiabjlcdef"); - buffer.edit(vec![4..9], "mno", None).unwrap(); - assert_eq!(buffer.text(), "ghiamnoef"); - buffer - }); - }) - } - - #[test] - fn test_edit_events() { - App::test((), |app| { - let mut now = Instant::now(); - let buffer_1_events = Rc::new(RefCell::new(Vec::new())); - let buffer_2_events = Rc::new(RefCell::new(Vec::new())); - - let buffer1 = app.add_model(|ctx| Buffer::new(0, "abcdef", ctx)); - let buffer2 = app.add_model(|ctx| Buffer::new(1, "abcdef", ctx)); - let mut buffer_ops = Vec::new(); - buffer1.update(app, |buffer, ctx| { - let buffer_1_events = buffer_1_events.clone(); - ctx.subscribe(&buffer1, move |_, event, _| { - buffer_1_events.borrow_mut().push(event.clone()) - }); - let buffer_2_events = buffer_2_events.clone(); - ctx.subscribe(&buffer2, move |_, event, _| { - buffer_2_events.borrow_mut().push(event.clone()) - }); - - // An edit emits an edited event, followed by a dirtied event, - // since the buffer was previously in a clean state. - let ops = buffer.edit(Some(2..4), "XYZ", Some(ctx)).unwrap(); - buffer_ops.extend_from_slice(&ops); - - // An empty transaction does not emit any events. - buffer.start_transaction(None).unwrap(); - buffer.end_transaction(None, Some(ctx)).unwrap(); - - // A transaction containing two edits emits one edited event. - now += Duration::from_secs(1); - buffer.start_transaction_at(None, now).unwrap(); - let ops = buffer.edit(Some(5..5), "u", Some(ctx)).unwrap(); - buffer_ops.extend_from_slice(&ops); - let ops = buffer.edit(Some(6..6), "w", Some(ctx)).unwrap(); - buffer_ops.extend_from_slice(&ops); - buffer.end_transaction_at(None, now, Some(ctx)).unwrap(); - - // Undoing a transaction emits one edited event. - let ops = buffer.undo(Some(ctx)); - buffer_ops.extend_from_slice(&ops); - }); - - // Incorporating a set of remote ops emits a single edited event, - // followed by a dirtied event. - buffer2.update(app, |buffer, ctx| { - buffer.apply_ops(buffer_ops, Some(ctx)).unwrap(); - }); - - let buffer_1_events = buffer_1_events.borrow(); - assert_eq!( - *buffer_1_events, - vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited] - ); - - let buffer_2_events = buffer_2_events.borrow(); - assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]); + #[gpui::test] + fn test_edit(ctx: &mut gpui::MutableAppContext) { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "abc", ctx); + assert_eq!(buffer.text(), "abc"); + buffer.edit(vec![3..3], "def", None).unwrap(); + assert_eq!(buffer.text(), "abcdef"); + buffer.edit(vec![0..0], "ghi", None).unwrap(); + assert_eq!(buffer.text(), "ghiabcdef"); + buffer.edit(vec![5..5], "jkl", None).unwrap(); + assert_eq!(buffer.text(), "ghiabjklcdef"); + buffer.edit(vec![6..7], "", None).unwrap(); + assert_eq!(buffer.text(), "ghiabjlcdef"); + buffer.edit(vec![4..9], "mno", None).unwrap(); + assert_eq!(buffer.text(), "ghiamnoef"); + buffer }); } - #[test] - fn test_random_edits() { + #[gpui::test] + fn test_edit_events(app: &mut gpui::MutableAppContext) { + let mut now = Instant::now(); + let buffer_1_events = Rc::new(RefCell::new(Vec::new())); + let buffer_2_events = Rc::new(RefCell::new(Vec::new())); + + let buffer1 = app.add_model(|ctx| Buffer::new(0, "abcdef", ctx)); + let buffer2 = app.add_model(|ctx| Buffer::new(1, "abcdef", ctx)); + let mut buffer_ops = Vec::new(); + buffer1.update(app, |buffer, ctx| { + let buffer_1_events = buffer_1_events.clone(); + ctx.subscribe(&buffer1, move |_, event, _| { + buffer_1_events.borrow_mut().push(event.clone()) + }); + let buffer_2_events = buffer_2_events.clone(); + ctx.subscribe(&buffer2, move |_, event, _| { + buffer_2_events.borrow_mut().push(event.clone()) + }); + + // An edit emits an edited event, followed by a dirtied event, + // since the buffer was previously in a clean state. + let ops = buffer.edit(Some(2..4), "XYZ", Some(ctx)).unwrap(); + buffer_ops.extend_from_slice(&ops); + + // An empty transaction does not emit any events. + buffer.start_transaction(None).unwrap(); + buffer.end_transaction(None, Some(ctx)).unwrap(); + + // A transaction containing two edits emits one edited event. + now += Duration::from_secs(1); + buffer.start_transaction_at(None, now).unwrap(); + let ops = buffer.edit(Some(5..5), "u", Some(ctx)).unwrap(); + buffer_ops.extend_from_slice(&ops); + let ops = buffer.edit(Some(6..6), "w", Some(ctx)).unwrap(); + buffer_ops.extend_from_slice(&ops); + buffer.end_transaction_at(None, now, Some(ctx)).unwrap(); + + // Undoing a transaction emits one edited event. + let ops = buffer.undo(Some(ctx)); + buffer_ops.extend_from_slice(&ops); + }); + + // Incorporating a set of remote ops emits a single edited event, + // followed by a dirtied event. + buffer2.update(app, |buffer, ctx| { + buffer.apply_ops(buffer_ops, Some(ctx)).unwrap(); + }); + + let buffer_1_events = buffer_1_events.borrow(); + assert_eq!( + *buffer_1_events, + vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited] + ); + + let buffer_2_events = buffer_2_events.borrow(); + assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]); + } + + #[gpui::test] + fn test_random_edits(ctx: &mut gpui::MutableAppContext) { for seed in 0..100 { - App::test((), |ctx| { - println!("{:?}", seed); - let mut rng = &mut StdRng::seed_from_u64(seed); + 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::(); - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, reference_string.as_str(), ctx); - let mut buffer_versions = Vec::new(); - for _i in 0..10 { - let (old_ranges, new_text, _) = buffer.randomly_mutate(rng, None); - for old_range in old_ranges.iter().rev() { - reference_string = [ - &reference_string[0..old_range.start], - new_text.as_str(), - &reference_string[old_range.end..], - ] - .concat(); - } - assert_eq!(buffer.text(), reference_string); + let reference_string_len = rng.gen_range(0..3); + let mut reference_string = RandomCharIter::new(&mut rng) + .take(reference_string_len) + .collect::(); + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, reference_string.as_str(), ctx); + let mut buffer_versions = Vec::new(); + for _i in 0..10 { + let (old_ranges, new_text, _) = buffer.randomly_mutate(rng, None); + for old_range in old_ranges.iter().rev() { + reference_string = [ + &reference_string[0..old_range.start], + new_text.as_str(), + &reference_string[old_range.end..], + ] + .concat(); + } + assert_eq!(buffer.text(), reference_string); - if rng.gen_bool(0.25) { - buffer.randomly_undo_redo(rng); - reference_string = buffer.text(); - } + if rng.gen_bool(0.25) { + buffer.randomly_undo_redo(rng); + reference_string = buffer.text(); + } - { - let line_lengths = line_lengths_in_range(&buffer, 0..buffer.len()); + { + let line_lengths = line_lengths_in_range(&buffer, 0..buffer.len()); - for (len, rows) in &line_lengths { - for row in rows { - assert_eq!(buffer.line_len(*row).unwrap(), *len); - } + for (len, rows) in &line_lengths { + for row in rows { + assert_eq!(buffer.line_len(*row).unwrap(), *len); } - - let (longest_column, longest_rows) = - line_lengths.iter().next_back().unwrap(); - let rightmost_point = buffer.rightmost_point(); - assert_eq!(rightmost_point.column, *longest_column); - assert!(longest_rows.contains(&rightmost_point.row)); } - for _ in 0..5 { - let end = rng.gen_range(0..buffer.len() + 1); - let start = rng.gen_range(0..end + 1); - - let line_lengths = line_lengths_in_range(&buffer, start..end); - let (longest_column, longest_rows) = - line_lengths.iter().next_back().unwrap(); - let range_sum = buffer.text_summary_for_range(start..end); - assert_eq!(range_sum.rightmost_point.column, *longest_column); - assert!(longest_rows.contains(&range_sum.rightmost_point.row)); - let range_text = &buffer.text()[start..end]; - assert_eq!(range_sum.chars, range_text.chars().count()); - assert_eq!(range_sum.bytes, range_text.len()); - } - - if rng.gen_bool(0.3) { - buffer_versions.push(buffer.clone()); - } + let (longest_column, longest_rows) = + line_lengths.iter().next_back().unwrap(); + let rightmost_point = buffer.rightmost_point(); + assert_eq!(rightmost_point.column, *longest_column); + assert!(longest_rows.contains(&rightmost_point.row)); } - for mut old_buffer in buffer_versions { - let mut delta = 0_isize; - for Edit { - old_range, - new_range, - } in buffer.edits_since(old_buffer.version.clone()) - { - let old_len = old_range.end - old_range.start; - let new_len = new_range.end - new_range.start; - let old_start = (old_range.start as isize + delta) as usize; - let new_text: String = - buffer.text_for_range(new_range).unwrap().collect(); - old_buffer - .edit(Some(old_start..old_start + old_len), new_text, None) - .unwrap(); + for _ in 0..5 { + let end = rng.gen_range(0..buffer.len() + 1); + let start = rng.gen_range(0..end + 1); - delta += new_len as isize - old_len as isize; - } - assert_eq!(old_buffer.text(), buffer.text()); + let line_lengths = line_lengths_in_range(&buffer, start..end); + let (longest_column, longest_rows) = + line_lengths.iter().next_back().unwrap(); + let range_sum = buffer.text_summary_for_range(start..end); + assert_eq!(range_sum.rightmost_point.column, *longest_column); + assert!(longest_rows.contains(&range_sum.rightmost_point.row)); + let range_text = &buffer.text()[start..end]; + assert_eq!(range_sum.chars, range_text.chars().count()); + assert_eq!(range_sum.bytes, range_text.len()); } - buffer - }) + 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.version.clone()) + { + let old_len = old_range.end - old_range.start; + let new_len = new_range.end - new_range.start; + let old_start = (old_range.start as isize + delta) as usize; + let new_text: String = buffer.text_for_range(new_range).unwrap().collect(); + old_buffer + .edit(Some(old_start..old_start + old_len), new_text, None) + .unwrap(); + + delta += new_len as isize - old_len as isize; + } + assert_eq!(old_buffer.text(), buffer.text()); + } + + buffer }); } } - #[test] - fn test_line_len() { - App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "", ctx); - buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap(); - buffer.edit(vec![12..12], "kl\nmno", None).unwrap(); - buffer.edit(vec![18..18], "\npqrs\n", None).unwrap(); - buffer.edit(vec![18..21], "\nPQ", None).unwrap(); + #[gpui::test] + fn test_line_len(ctx: &mut gpui::MutableAppContext) { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "", ctx); + buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap(); + buffer.edit(vec![12..12], "kl\nmno", None).unwrap(); + buffer.edit(vec![18..18], "\npqrs\n", None).unwrap(); + buffer.edit(vec![18..21], "\nPQ", None).unwrap(); - assert_eq!(buffer.line_len(0).unwrap(), 4); - assert_eq!(buffer.line_len(1).unwrap(), 3); - assert_eq!(buffer.line_len(2).unwrap(), 5); - assert_eq!(buffer.line_len(3).unwrap(), 3); - assert_eq!(buffer.line_len(4).unwrap(), 4); - assert_eq!(buffer.line_len(5).unwrap(), 0); - assert!(buffer.line_len(6).is_err()); - buffer - }); + assert_eq!(buffer.line_len(0).unwrap(), 4); + assert_eq!(buffer.line_len(1).unwrap(), 3); + assert_eq!(buffer.line_len(2).unwrap(), 5); + assert_eq!(buffer.line_len(3).unwrap(), 3); + assert_eq!(buffer.line_len(4).unwrap(), 4); + assert_eq!(buffer.line_len(5).unwrap(), 0); + assert!(buffer.line_len(6).is_err()); + buffer }); } - #[test] - fn test_rightmost_point() { - App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "", ctx); - assert_eq!(buffer.rightmost_point().row, 0); - buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap(); - assert_eq!(buffer.rightmost_point().row, 0); - buffer.edit(vec![12..12], "kl\nmno", None).unwrap(); - assert_eq!(buffer.rightmost_point().row, 2); - buffer.edit(vec![18..18], "\npqrs", None).unwrap(); - assert_eq!(buffer.rightmost_point().row, 2); - buffer.edit(vec![10..12], "", None).unwrap(); - assert_eq!(buffer.rightmost_point().row, 0); - buffer.edit(vec![24..24], "tuv", None).unwrap(); - assert_eq!(buffer.rightmost_point().row, 4); - buffer - }); + #[gpui::test] + fn test_rightmost_point(ctx: &mut gpui::MutableAppContext) { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "", ctx); + assert_eq!(buffer.rightmost_point().row, 0); + buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap(); + assert_eq!(buffer.rightmost_point().row, 0); + buffer.edit(vec![12..12], "kl\nmno", None).unwrap(); + assert_eq!(buffer.rightmost_point().row, 2); + buffer.edit(vec![18..18], "\npqrs", None).unwrap(); + assert_eq!(buffer.rightmost_point().row, 2); + buffer.edit(vec![10..12], "", None).unwrap(); + assert_eq!(buffer.rightmost_point().row, 0); + buffer.edit(vec![24..24], "tuv", None).unwrap(); + assert_eq!(buffer.rightmost_point().row, 4); + buffer }); } - #[test] - fn test_text_summary_for_range() { - App::test((), |ctx| { - ctx.add_model(|ctx| { - let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz", ctx); - let text = Text::from(buffer.text()); - assert_eq!( - buffer.text_summary_for_range(1..3), - text.slice(1..3).summary() - ); - assert_eq!( - buffer.text_summary_for_range(1..12), - text.slice(1..12).summary() - ); - assert_eq!( - buffer.text_summary_for_range(0..20), - text.slice(0..20).summary() - ); - assert_eq!( - buffer.text_summary_for_range(0..22), - text.slice(0..22).summary() - ); - assert_eq!( - buffer.text_summary_for_range(7..22), - text.slice(7..22).summary() - ); - buffer - }); + #[gpui::test] + fn test_text_summary_for_range(ctx: &mut gpui::MutableAppContext) { + ctx.add_model(|ctx| { + let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz", ctx); + let text = Text::from(buffer.text()); + assert_eq!( + buffer.text_summary_for_range(1..3), + text.slice(1..3).summary() + ); + assert_eq!( + buffer.text_summary_for_range(1..12), + text.slice(1..12).summary() + ); + assert_eq!( + buffer.text_summary_for_range(0..20), + text.slice(0..20).summary() + ); + assert_eq!( + buffer.text_summary_for_range(0..22), + text.slice(0..22).summary() + ); + assert_eq!( + buffer.text_summary_for_range(7..22), + text.slice(7..22).summary() + ); + buffer }); } - #[test] - fn test_chars_at() { - App::test((), |ctx| { - ctx.add_model(|ctx| { + #[gpui::test] + fn test_chars_at(ctx: &mut gpui::MutableAppContext) { + ctx.add_model(|ctx| { let mut buffer = Buffer::new(0, "", ctx); buffer.edit(vec![0..0], "abcd\nefgh\nij", None).unwrap(); buffer.edit(vec![12..12], "kl\nmno", None).unwrap(); @@ -2677,7 +2663,6 @@ mod tests { buffer }); - }); } // #[test] @@ -2794,196 +2779,192 @@ mod tests { } } - #[test] - fn test_anchors() { - App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "", ctx); - buffer.edit(vec![0..0], "abc", None).unwrap(); - let left_anchor = buffer.anchor_before(2).unwrap(); - let right_anchor = buffer.anchor_after(2).unwrap(); + #[gpui::test] + fn test_anchors(ctx: &mut gpui::MutableAppContext) { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "", ctx); + buffer.edit(vec![0..0], "abc", None).unwrap(); + let left_anchor = buffer.anchor_before(2).unwrap(); + let right_anchor = buffer.anchor_after(2).unwrap(); - buffer.edit(vec![1..1], "def\n", None).unwrap(); - assert_eq!(buffer.text(), "adef\nbc"); - assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 6); - assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 6); - 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(vec![1..1], "def\n", None).unwrap(); + assert_eq!(buffer.text(), "adef\nbc"); + assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 6); + assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 6); + 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(vec![2..3], "", None).unwrap(); - assert_eq!(buffer.text(), "adf\nbc"); - assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); - assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 5); - 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(vec![2..3], "", None).unwrap(); + assert_eq!(buffer.text(), "adf\nbc"); + assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); + assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 5); + 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(vec![5..5], "ghi\n", None).unwrap(); - assert_eq!(buffer.text(), "adf\nbghi\nc"); - assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); - assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 9); - 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(vec![5..5], "ghi\n", None).unwrap(); + assert_eq!(buffer.text(), "adf\nbghi\nc"); + assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); + assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 9); + 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(vec![7..9], "", None).unwrap(); - assert_eq!(buffer.text(), "adf\nbghc"); - assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); - assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 7); - 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 } - ); + buffer.edit(vec![7..9], "", None).unwrap(); + assert_eq!(buffer.text(), "adf\nbghc"); + assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); + assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 7); + 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 }).unwrap(), - buffer.anchor_before(0).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 1 }).unwrap(), - buffer.anchor_before(1).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 2 }).unwrap(), - buffer.anchor_before(2).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 3 }).unwrap(), - buffer.anchor_before(3).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 0 }).unwrap(), - buffer.anchor_before(4).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 1 }).unwrap(), - buffer.anchor_before(5).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 2 }).unwrap(), - buffer.anchor_before(6).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 3 }).unwrap(), - buffer.anchor_before(7).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 4 }).unwrap(), - buffer.anchor_before(8).unwrap() - ); + // Ensure anchoring to a point is equivalent to anchoring to an offset. + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 0 }).unwrap(), + buffer.anchor_before(0).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 1 }).unwrap(), + buffer.anchor_before(1).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 2 }).unwrap(), + buffer.anchor_before(2).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 3 }).unwrap(), + buffer.anchor_before(3).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 0 }).unwrap(), + buffer.anchor_before(4).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 1 }).unwrap(), + buffer.anchor_before(5).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 2 }).unwrap(), + buffer.anchor_before(6).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 3 }).unwrap(), + buffer.anchor_before(7).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 4 }).unwrap(), + buffer.anchor_before(8).unwrap() + ); - // 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(); + // 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) - .unwrap(), - Ordering::Equal - ); - assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), - Ordering::Equal - ); - assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), - Ordering::Equal - ); + assert_eq!( + anchor_at_offset_0 + .cmp(&anchor_at_offset_0, &buffer) + .unwrap(), + Ordering::Equal + ); + assert_eq!( + anchor_at_offset_1 + .cmp(&anchor_at_offset_1, &buffer) + .unwrap(), + Ordering::Equal + ); + assert_eq!( + anchor_at_offset_2 + .cmp(&anchor_at_offset_2, &buffer) + .unwrap(), + Ordering::Equal + ); - assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), - Ordering::Less - ); - assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), - Ordering::Less - ); - assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), - Ordering::Less - ); + assert_eq!( + anchor_at_offset_0 + .cmp(&anchor_at_offset_1, &buffer) + .unwrap(), + Ordering::Less + ); + assert_eq!( + anchor_at_offset_1 + .cmp(&anchor_at_offset_2, &buffer) + .unwrap(), + Ordering::Less + ); + assert_eq!( + anchor_at_offset_0 + .cmp(&anchor_at_offset_2, &buffer) + .unwrap(), + Ordering::Less + ); - assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), - Ordering::Greater - ); - assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), - Ordering::Greater - ); - assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), - Ordering::Greater - ); - buffer - }); + assert_eq!( + anchor_at_offset_1 + .cmp(&anchor_at_offset_0, &buffer) + .unwrap(), + Ordering::Greater + ); + assert_eq!( + anchor_at_offset_2 + .cmp(&anchor_at_offset_1, &buffer) + .unwrap(), + Ordering::Greater + ); + assert_eq!( + anchor_at_offset_2 + .cmp(&anchor_at_offset_0, &buffer) + .unwrap(), + Ordering::Greater + ); + buffer }); } - #[test] - fn test_anchors_at_start_and_end() { - App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "", ctx); - let before_start_anchor = buffer.anchor_before(0).unwrap(); - let after_end_anchor = buffer.anchor_after(0).unwrap(); + #[gpui::test] + fn test_anchors_at_start_and_end(ctx: &mut gpui::MutableAppContext) { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "", ctx); + let before_start_anchor = buffer.anchor_before(0).unwrap(); + let after_end_anchor = buffer.anchor_after(0).unwrap(); - buffer.edit(vec![0..0], "abc", None).unwrap(); - assert_eq!(buffer.text(), "abc"); - assert_eq!(before_start_anchor.to_offset(&buffer).unwrap(), 0); - assert_eq!(after_end_anchor.to_offset(&buffer).unwrap(), 3); + buffer.edit(vec![0..0], "abc", None).unwrap(); + assert_eq!(buffer.text(), "abc"); + assert_eq!(before_start_anchor.to_offset(&buffer).unwrap(), 0); + assert_eq!(after_end_anchor.to_offset(&buffer).unwrap(), 3); - let after_start_anchor = buffer.anchor_after(0).unwrap(); - let before_end_anchor = buffer.anchor_before(3).unwrap(); + let after_start_anchor = buffer.anchor_after(0).unwrap(); + let before_end_anchor = buffer.anchor_before(3).unwrap(); - buffer.edit(vec![3..3], "def", None).unwrap(); - buffer.edit(vec![0..0], "ghi", None).unwrap(); - assert_eq!(buffer.text(), "ghiabcdef"); - assert_eq!(before_start_anchor.to_offset(&buffer).unwrap(), 0); - assert_eq!(after_start_anchor.to_offset(&buffer).unwrap(), 3); - assert_eq!(before_end_anchor.to_offset(&buffer).unwrap(), 6); - assert_eq!(after_end_anchor.to_offset(&buffer).unwrap(), 9); - buffer - }); + buffer.edit(vec![3..3], "def", None).unwrap(); + buffer.edit(vec![0..0], "ghi", None).unwrap(); + assert_eq!(buffer.text(), "ghiabcdef"); + assert_eq!(before_start_anchor.to_offset(&buffer).unwrap(), 0); + assert_eq!(after_start_anchor.to_offset(&buffer).unwrap(), 3); + assert_eq!(before_end_anchor.to_offset(&buffer).unwrap(), 6); + assert_eq!(after_end_anchor.to_offset(&buffer).unwrap(), 9); + buffer }); } @@ -3105,115 +3086,111 @@ mod tests { }); } - #[test] - fn test_undo_redo() { - App::test((), |app| { - app.add_model(|ctx| { - let mut buffer = Buffer::new(0, "1234", ctx); + #[gpui::test] + fn test_undo_redo(app: &mut gpui::MutableAppContext) { + app.add_model(|ctx| { + let mut buffer = Buffer::new(0, "1234", ctx); - let edit1 = buffer.edit(vec![1..1], "abx", None).unwrap(); - let edit2 = buffer.edit(vec![3..4], "yzef", None).unwrap(); - let edit3 = buffer.edit(vec![3..5], "cd", None).unwrap(); - assert_eq!(buffer.text(), "1abcdef234"); + let edit1 = buffer.edit(vec![1..1], "abx", None).unwrap(); + let edit2 = buffer.edit(vec![3..4], "yzef", None).unwrap(); + let edit3 = buffer.edit(vec![3..5], "cd", None).unwrap(); + assert_eq!(buffer.text(), "1abcdef234"); - buffer.undo_or_redo(edit1[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1cdef234"); - buffer.undo_or_redo(edit1[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1abcdef234"); + buffer.undo_or_redo(edit1[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1cdef234"); + buffer.undo_or_redo(edit1[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abcdef234"); - buffer.undo_or_redo(edit2[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1abcdx234"); - buffer.undo_or_redo(edit3[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1abx234"); - buffer.undo_or_redo(edit2[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1abyzef234"); - buffer.undo_or_redo(edit3[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1abcdef234"); + buffer.undo_or_redo(edit2[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abcdx234"); + buffer.undo_or_redo(edit3[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abx234"); + buffer.undo_or_redo(edit2[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abyzef234"); + buffer.undo_or_redo(edit3[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abcdef234"); - buffer.undo_or_redo(edit3[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1abyzef234"); - buffer.undo_or_redo(edit1[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1yzef234"); - buffer.undo_or_redo(edit2[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1234"); + buffer.undo_or_redo(edit3[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abyzef234"); + buffer.undo_or_redo(edit1[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1yzef234"); + buffer.undo_or_redo(edit2[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1234"); - buffer - }); + buffer }); } - #[test] - fn test_history() { - App::test((), |app| { - app.add_model(|ctx| { - let mut now = Instant::now(); - let mut buffer = Buffer::new(0, "123456", ctx); + #[gpui::test] + fn test_history(app: &mut gpui::MutableAppContext) { + app.add_model(|ctx| { + let mut now = Instant::now(); + let mut buffer = Buffer::new(0, "123456", ctx); - let (set_id, _) = buffer - .add_selection_set(buffer.selections_from_ranges(vec![4..4]).unwrap(), None); - buffer.start_transaction_at(Some(set_id), now).unwrap(); - buffer.edit(vec![2..4], "cd", None).unwrap(); - buffer.end_transaction_at(Some(set_id), now, None).unwrap(); - assert_eq!(buffer.text(), "12cd56"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); + let (set_id, _) = + buffer.add_selection_set(buffer.selections_from_ranges(vec![4..4]).unwrap(), None); + buffer.start_transaction_at(Some(set_id), now).unwrap(); + buffer.edit(vec![2..4], "cd", None).unwrap(); + buffer.end_transaction_at(Some(set_id), now, None).unwrap(); + assert_eq!(buffer.text(), "12cd56"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); - buffer.start_transaction_at(Some(set_id), now).unwrap(); - buffer - .update_selection_set( - set_id, - buffer.selections_from_ranges(vec![1..3]).unwrap(), - None, - ) - .unwrap(); - buffer.edit(vec![4..5], "e", None).unwrap(); - buffer.end_transaction_at(Some(set_id), now, None).unwrap(); - assert_eq!(buffer.text(), "12cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); + buffer.start_transaction_at(Some(set_id), now).unwrap(); + buffer + .update_selection_set( + set_id, + buffer.selections_from_ranges(vec![1..3]).unwrap(), + None, + ) + .unwrap(); + buffer.edit(vec![4..5], "e", None).unwrap(); + buffer.end_transaction_at(Some(set_id), now, None).unwrap(); + assert_eq!(buffer.text(), "12cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); - now += UNDO_GROUP_INTERVAL + Duration::from_millis(1); - buffer.start_transaction_at(Some(set_id), now).unwrap(); - buffer - .update_selection_set( - set_id, - buffer.selections_from_ranges(vec![2..2]).unwrap(), - None, - ) - .unwrap(); - buffer.edit(vec![0..1], "a", None).unwrap(); - buffer.edit(vec![1..1], "b", None).unwrap(); - buffer.end_transaction_at(Some(set_id), now, None).unwrap(); - assert_eq!(buffer.text(), "ab2cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); + now += UNDO_GROUP_INTERVAL + Duration::from_millis(1); + buffer.start_transaction_at(Some(set_id), now).unwrap(); + buffer + .update_selection_set( + set_id, + buffer.selections_from_ranges(vec![2..2]).unwrap(), + None, + ) + .unwrap(); + buffer.edit(vec![0..1], "a", None).unwrap(); + buffer.edit(vec![1..1], "b", None).unwrap(); + buffer.end_transaction_at(Some(set_id), now, None).unwrap(); + assert_eq!(buffer.text(), "ab2cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); - // Last transaction happened past the group interval, undo it on its - // own. - buffer.undo(None); - assert_eq!(buffer.text(), "12cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); + // Last transaction happened past the group interval, undo it on its + // own. + buffer.undo(None); + assert_eq!(buffer.text(), "12cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); - // First two transactions happened within the group interval, undo them - // together. - buffer.undo(None); - assert_eq!(buffer.text(), "123456"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); + // First two transactions happened within the group interval, undo them + // together. + buffer.undo(None); + assert_eq!(buffer.text(), "123456"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); - // Redo the first two transactions together. - buffer.redo(None); - assert_eq!(buffer.text(), "12cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); + // Redo the first two transactions together. + buffer.redo(None); + assert_eq!(buffer.text(), "12cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); - // Redo the last transaction on its own. - buffer.redo(None); - assert_eq!(buffer.text(), "ab2cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); + // Redo the last transaction on its own. + buffer.redo(None); + assert_eq!(buffer.text(), "ab2cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); - buffer - }); + buffer }); } - #[test] - fn test_random_concurrent_edits() { + #[gpui::test] + fn test_random_concurrent_edits(ctx: &mut gpui::MutableAppContext) { use crate::test::Network; const PEERS: usize = 5; @@ -3222,66 +3199,64 @@ mod tests { println!("{:?}", seed); let mut rng = &mut StdRng::seed_from_u64(seed); - App::test((), |ctx| { - 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 0..PEERS { - let buffer = - ctx.add_model(|ctx| Buffer::new(i as ReplicaId, base_text.as_str(), ctx)); - buffers.push(buffer); - replica_ids.push(i as u16); - network.add_peer(i as u16); - } + 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 0..PEERS { + let buffer = + ctx.add_model(|ctx| Buffer::new(i as ReplicaId, base_text.as_str(), ctx)); + buffers.push(buffer); + replica_ids.push(i as u16); + network.add_peer(i as u16); + } - let mut mutation_count = 10; - loop { - let replica_index = rng.gen_range(0..PEERS); - let replica_id = replica_ids[replica_index]; - buffers[replica_index].update(ctx, |buffer, _| match rng.gen_range(0..=100) { - 0..=50 if mutation_count != 0 => { - let (_, _, ops) = buffer.randomly_mutate(&mut rng, None); - network.broadcast(replica_id, ops, &mut rng); - mutation_count -= 1; - } - 51..=70 if mutation_count != 0 => { - let ops = buffer.randomly_undo_redo(&mut rng); - network.broadcast(replica_id, ops, &mut rng); - mutation_count -= 1; - } - 71..=100 if network.has_unreceived(replica_id) => { - buffer - .apply_ops(network.receive(replica_id, &mut rng), None) - .unwrap(); - } - _ => {} - }); - - if mutation_count == 0 && network.is_idle() { - break; + let mut mutation_count = 10; + loop { + let replica_index = rng.gen_range(0..PEERS); + let replica_id = replica_ids[replica_index]; + buffers[replica_index].update(ctx, |buffer, _| match rng.gen_range(0..=100) { + 0..=50 if mutation_count != 0 => { + let (_, _, ops) = buffer.randomly_mutate(&mut rng, None); + network.broadcast(replica_id, ops, &mut rng); + mutation_count -= 1; } - } + 51..=70 if mutation_count != 0 => { + let ops = buffer.randomly_undo_redo(&mut rng); + network.broadcast(replica_id, ops, &mut rng); + mutation_count -= 1; + } + 71..=100 if network.has_unreceived(replica_id) => { + buffer + .apply_ops(network.receive(replica_id, &mut rng), None) + .unwrap(); + } + _ => {} + }); - let first_buffer = buffers[0].read(ctx); - for buffer in &buffers[1..] { - let buffer = buffer.read(ctx); - assert_eq!(buffer.text(), first_buffer.text()); - assert_eq!( - buffer.all_selections().collect::>(), - first_buffer.all_selections().collect::>() - ); - assert_eq!( - buffer.all_selection_ranges().collect::>(), - first_buffer - .all_selection_ranges() - .collect::>() - ); + if mutation_count == 0 && network.is_idle() { + break; } - }); + } + + let first_buffer = buffers[0].read(ctx); + for buffer in &buffers[1..] { + let buffer = buffer.read(ctx); + assert_eq!(buffer.text(), first_buffer.text()); + assert_eq!( + buffer.all_selections().collect::>(), + first_buffer.all_selections().collect::>() + ); + assert_eq!( + buffer.all_selection_ranges().collect::>(), + first_buffer + .all_selection_ranges() + .collect::>() + ); + } } } diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index a76f558b96..2e54336e68 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -2511,222 +2511,207 @@ impl workspace::ItemView for BufferView { mod tests { use super::*; use crate::{editor::Point, settings, test::sample_text}; - use gpui::App; use unindent::Unindent; - #[test] - fn test_selection_with_mouse() { - App::test((), |app| { - let buffer = - app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, buffer_view) = - app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + #[gpui::test] + fn test_selection_with_mouse(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, buffer_view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - buffer_view.update(app, |view, ctx| { - view.begin_selection(DisplayPoint::new(2, 2), false, ctx); - }); - - let view = buffer_view.read(app); - let selections = view - .selections_in_range( - DisplayPoint::zero()..view.max_point(app.as_ref()), - app.as_ref(), - ) - .collect::>(); - assert_eq!( - selections, - [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] - ); - - buffer_view.update(app, |view, ctx| { - view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx); - }); - - let view = buffer_view.read(app); - let selections = view - .selections_in_range( - DisplayPoint::zero()..view.max_point(app.as_ref()), - app.as_ref(), - ) - .collect::>(); - assert_eq!( - selections, - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); - - buffer_view.update(app, |view, ctx| { - view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx); - }); - - let view = buffer_view.read(app); - let selections = view - .selections_in_range( - DisplayPoint::zero()..view.max_point(app.as_ref()), - app.as_ref(), - ) - .collect::>(); - assert_eq!( - selections, - [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] - ); - - buffer_view.update(app, |view, ctx| { - view.end_selection(ctx); - view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx); - }); - - let view = buffer_view.read(app); - let selections = view - .selections_in_range( - DisplayPoint::zero()..view.max_point(app.as_ref()), - app.as_ref(), - ) - .collect::>(); - assert_eq!( - selections, - [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] - ); - - buffer_view.update(app, |view, ctx| { - view.begin_selection(DisplayPoint::new(3, 3), true, ctx); - view.update_selection(DisplayPoint::new(0, 0), Vector2F::zero(), ctx); - }); - - let view = buffer_view.read(app); - let selections = view - .selections_in_range( - DisplayPoint::zero()..view.max_point(app.as_ref()), - app.as_ref(), - ) - .collect::>(); - assert_eq!( - selections, - [ - DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) - ] - ); - - buffer_view.update(app, |view, ctx| { - view.end_selection(ctx); - }); - - let view = buffer_view.read(app); - let selections = view - .selections_in_range( - DisplayPoint::zero()..view.max_point(app.as_ref()), - app.as_ref(), - ) - .collect::>(); - assert_eq!( - selections, - [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] - ); + buffer_view.update(app, |view, ctx| { + view.begin_selection(DisplayPoint::new(2, 2), false, ctx); }); - } - #[test] - fn test_canceling_pending_selection() { - App::test((), |app| { - let buffer = - app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + let view = buffer_view.read(app); + let selections = view + .selections_in_range( + DisplayPoint::zero()..view.max_point(app.as_ref()), + app.as_ref(), + ) + .collect::>(); + assert_eq!( + selections, + [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] + ); - view.update(app, |view, ctx| { - view.begin_selection(DisplayPoint::new(2, 2), false, ctx); - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] - ); - - view.update(app, |view, ctx| { - view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx); - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); - - view.update(app, |view, ctx| { - view.cancel(&(), ctx); - view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx); - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); + buffer_view.update(app, |view, ctx| { + view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx); }); - } - #[test] - fn test_cancel() { - App::test((), |app| { - let buffer = - app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + let view = buffer_view.read(app); + let selections = view + .selections_in_range( + DisplayPoint::zero()..view.max_point(app.as_ref()), + app.as_ref(), + ) + .collect::>(); + assert_eq!( + selections, + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); - view.update(app, |view, ctx| { - view.begin_selection(DisplayPoint::new(3, 4), false, ctx); - view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx); - view.end_selection(ctx); - - view.begin_selection(DisplayPoint::new(0, 1), true, ctx); - view.update_selection(DisplayPoint::new(0, 3), Vector2F::zero(), ctx); - view.end_selection(ctx); - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1), - ] - ); - - view.update(app, |view, ctx| view.cancel(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] - ); - - view.update(app, |view, ctx| view.cancel(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] - ); + buffer_view.update(app, |view, ctx| { + view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx); }); + + let view = buffer_view.read(app); + let selections = view + .selections_in_range( + DisplayPoint::zero()..view.max_point(app.as_ref()), + app.as_ref(), + ) + .collect::>(); + assert_eq!( + selections, + [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] + ); + + buffer_view.update(app, |view, ctx| { + view.end_selection(ctx); + view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx); + }); + + let view = buffer_view.read(app); + let selections = view + .selections_in_range( + DisplayPoint::zero()..view.max_point(app.as_ref()), + app.as_ref(), + ) + .collect::>(); + assert_eq!( + selections, + [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] + ); + + buffer_view.update(app, |view, ctx| { + view.begin_selection(DisplayPoint::new(3, 3), true, ctx); + view.update_selection(DisplayPoint::new(0, 0), Vector2F::zero(), ctx); + }); + + let view = buffer_view.read(app); + let selections = view + .selections_in_range( + DisplayPoint::zero()..view.max_point(app.as_ref()), + app.as_ref(), + ) + .collect::>(); + assert_eq!( + selections, + [ + DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) + ] + ); + + buffer_view.update(app, |view, ctx| { + view.end_selection(ctx); + }); + + let view = buffer_view.read(app); + let selections = view + .selections_in_range( + DisplayPoint::zero()..view.max_point(app.as_ref()), + app.as_ref(), + ) + .collect::>(); + assert_eq!( + selections, + [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] + ); } - #[test] - fn test_layout_line_numbers() { - App::test((), |app| { - let layout_cache = TextLayoutCache::new(app.platform().fonts()); - let font_cache = app.font_cache().clone(); + #[gpui::test] + fn test_canceling_pending_selection(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx)); + view.update(app, |view, ctx| { + view.begin_selection(DisplayPoint::new(2, 2), false, ctx); + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] + ); - let settings = settings::channel(&font_cache).unwrap().1; - let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + view.update(app, |view, ctx| { + view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx); + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); - let layouts = view - .read(app) - .layout_line_numbers(1000.0, &font_cache, &layout_cache, app.as_ref()) - .unwrap(); - assert_eq!(layouts.len(), 6); - }) + view.update(app, |view, ctx| { + view.cancel(&(), ctx); + view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx); + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); } - #[test] - fn test_fold() { - App::test((), |app| { - let buffer = app.add_model(|ctx| { - Buffer::new( - 0, - " + #[gpui::test] + fn test_cancel(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + + view.update(app, |view, ctx| { + view.begin_selection(DisplayPoint::new(3, 4), false, ctx); + view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx); + view.end_selection(ctx); + + view.begin_selection(DisplayPoint::new(0, 1), true, ctx); + view.update_selection(DisplayPoint::new(0, 3), Vector2F::zero(), ctx); + view.end_selection(ctx); + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1), + ] + ); + + view.update(app, |view, ctx| view.cancel(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] + ); + + view.update(app, |view, ctx| view.cancel(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] + ); + } + + #[gpui::test] + fn test_layout_line_numbers(app: &mut gpui::MutableAppContext) { + let layout_cache = TextLayoutCache::new(app.platform().fonts()); + let font_cache = app.font_cache().clone(); + + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx)); + + let settings = settings::channel(&font_cache).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + + let layouts = view + .read(app) + .layout_line_numbers(1000.0, &font_cache, &layout_cache, app.as_ref()) + .unwrap(); + assert_eq!(layouts.len(), 6); + } + + #[gpui::test] + fn test_fold(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| { + Buffer::new( + 0, + " impl Foo { // Hello! @@ -2743,24 +2728,20 @@ mod tests { } } " - .unindent(), - ctx, - ) - }); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + .unindent(), + ctx, + ) + }); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)], - ctx, - ) + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)], ctx) .unwrap(); - view.fold(&(), ctx); - assert_eq!( - view.text(ctx.as_ref()), - " + view.fold(&(), ctx); + assert_eq!( + view.text(ctx.as_ref()), + " impl Foo { // Hello! @@ -2775,23 +2756,23 @@ mod tests { } } " - .unindent(), - ); + .unindent(), + ); - view.fold(&(), ctx); - assert_eq!( - view.text(ctx.as_ref()), - " + view.fold(&(), ctx); + assert_eq!( + view.text(ctx.as_ref()), + " impl Foo {… } " - .unindent(), - ); + .unindent(), + ); - view.unfold(&(), ctx); - assert_eq!( - view.text(ctx.as_ref()), - " + view.unfold(&(), ctx); + assert_eq!( + view.text(ctx.as_ref()), + " impl Foo { // Hello! @@ -2806,1109 +2787,1055 @@ mod tests { } } " - .unindent(), - ); + .unindent(), + ); - view.unfold(&(), ctx); - assert_eq!(view.text(ctx.as_ref()), buffer.read(ctx).text()); - }); + view.unfold(&(), ctx); + assert_eq!(view.text(ctx.as_ref()), buffer.read(ctx).text()); }); } - #[test] - fn test_move_cursor() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + #[gpui::test] + fn test_move_cursor(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); - buffer.update(app, |buffer, ctx| { - buffer - .edit( - vec![ - Point::new(1, 0)..Point::new(1, 0), - Point::new(1, 1)..Point::new(1, 1), - ], - "\t", - Some(ctx), - ) - .unwrap(); - }); - - view.update(app, |view, ctx| { - view.move_down(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] - ); - - view.move_right(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] - ); - - view.move_left(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] - ); - - view.move_up(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.move_to_end(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] - ); - - view.move_to_beginning(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.select_display_ranges( - &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)], - ctx, - ) - .unwrap(); - view.select_to_beginning(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] - ); - - view.select_to_end(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] - ); - }); - }); - } - - #[test] - fn test_beginning_end_of_line() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\n def", ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), - ], - ctx, - ) - .unwrap(); - }); - - view.update(app, |view, ctx| view.move_to_beginning_of_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - ] - ); - - view.update(app, |view, ctx| view.move_to_beginning_of_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - - view.update(app, |view, ctx| view.move_to_beginning_of_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - ] - ); - - view.update(app, |view, ctx| view.move_to_end_of_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), - ] - ); - - // Moving to the end of line again is a no-op. - view.update(app, |view, ctx| view.move_to_end_of_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), - ] - ); - - view.update(app, |view, ctx| { - view.move_left(&(), ctx); - view.select_to_beginning_of_line(&true, ctx); - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), - ] - ); - - view.update(app, |view, ctx| { - view.select_to_beginning_of_line(&true, ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0), - ] - ); - - view.update(app, |view, ctx| { - view.select_to_beginning_of_line(&true, ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), - ] - ); - - view.update(app, |view, ctx| view.select_to_end_of_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5), - ] - ); - - view.update(app, |view, ctx| view.delete_to_end_of_line(&(), ctx)); - assert_eq!(view.read(app).text(app.as_ref()), "ab\n de"); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), - ] - ); - - view.update(app, |view, ctx| view.delete_to_beginning_of_line(&(), ctx)); - assert_eq!(view.read(app).text(app.as_ref()), "\n"); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - }); - } - - #[test] - fn test_prev_next_word_boundary() { - App::test((), |app| { - let buffer = app - .add_model(|ctx| Buffer::new(0, "use std::str::{foo, bar}\n\n {baz.qux()}", ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), - ], - ctx, - ) - .unwrap(); - }); - - view.update(app, |view, ctx| { - view.move_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), - ] - ); - - view.update(app, |view, ctx| { - view.move_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), - DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), - ] - ); - - view.update(app, |view, ctx| { - view.move_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), - ] - ); - - view.update(app, |view, ctx| { - view.move_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - - view.update(app, |view, ctx| { - view.move_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), - ] - ); - - view.update(app, |view, ctx| { - view.move_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(0, 23)..DisplayPoint::new(0, 23), - ] - ); - - view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), - ] - ); - - view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - - view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), - ] - ); - - view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), - ] - ); - - view.update(app, |view, ctx| { - view.move_right(&(), ctx); - view.select_to_previous_word_boundary(&(), ctx); - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 2), - ] - ); - - view.update(app, |view, ctx| { - view.select_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 7), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 0), - ] - ); - - view.update(app, |view, ctx| view.select_to_next_word_boundary(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 2), - ] - ); - - view.update(app, |view, ctx| view.delete_to_next_word_boundary(&(), ctx)); - assert_eq!( - view.read(app).text(app.as_ref()), - "use std::s::{foo, bar}\n\n {az.qux()}" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 10), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), - ] - ); - - view.update(app, |view, ctx| { - view.delete_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "use std::::{foo, bar}\n\n az.qux()}" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), - ] - ); - }); - } - - #[test] - fn test_backspace() { - App::test((), |app| { - let buffer = app.add_model(|ctx| { - Buffer::new( - 0, - "one two three\nfour five six\nseven eight nine\nten\n", - ctx, - ) - }); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); - - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - // an empty selection - the preceding character is deleted - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - // one character selected - it is deleted - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - // a line suffix selected - it is deleted - DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), - ], - ctx, - ) - .unwrap(); - view.backspace(&(), ctx); - }); - - assert_eq!( - buffer.read(app).text(), - "oe two three\nfou five six\nseven ten\n" - ); - }) - } - - #[test] - fn test_delete() { - App::test((), |app| { - let buffer = app.add_model(|ctx| { - Buffer::new( - 0, - "one two three\nfour five six\nseven eight nine\nten\n", - ctx, - ) - }); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); - - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - // an empty selection - the following character is deleted - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - // one character selected - it is deleted - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - // a line suffix selected - it is deleted - DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), - ], - ctx, - ) - .unwrap(); - view.delete(&(), ctx); - }); - - assert_eq!( - buffer.read(app).text(), - "on two three\nfou five six\nseven ten\n" - ); - }) - } - - #[test] - fn test_delete_line() { - App::test((), |app| { - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - ], - ctx, - ) - .unwrap(); - view.delete_line(&(), ctx); - }); - assert_eq!(view.read(app).text(app.as_ref()), "ghi"); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) - ] - ); - - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], - ctx, - ) - .unwrap(); - view.delete_line(&(), ctx); - }); - assert_eq!(view.read(app).text(app.as_ref()), "ghi\n"); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] - ); - }); - } - - #[test] - fn test_duplicate_line() { - App::test((), |app| { - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - ], - ctx, - ) - .unwrap(); - view.duplicate_line(&(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "abc\nabc\ndef\ndef\nghi\n\n" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0), - ] - ); - - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1), - ], - ctx, - ) - .unwrap(); - view.duplicate_line(&(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "abc\ndef\nghi\nabc\ndef\nghi\n" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1), - DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1), - ] - ); - }); - } - - #[test] - fn test_move_line_up_down() { - App::test((), |app| { - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(10, 5), ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.fold_ranges( + buffer.update(app, |buffer, ctx| { + buffer + .edit( vec![ - Point::new(0, 2)..Point::new(1, 2), - Point::new(2, 3)..Point::new(4, 1), - Point::new(7, 0)..Point::new(8, 4), + Point::new(1, 0)..Point::new(1, 0), + Point::new(1, 1)..Point::new(1, 1), ], - ctx, - ); - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(4, 2), - DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2), - ], - ctx, + "\t", + Some(ctx), ) .unwrap(); - }); + }); + + view.update(app, |view, ctx| { + view.move_down(&(), ctx); assert_eq!( - view.read(app).text(app.as_ref()), - "aa…bbb\nccc…eeee\nfffff\nggggg\n…i\njjjjj" + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] ); - view.update(app, |view, ctx| view.move_line_up(&(), ctx)); + view.move_right(&(), ctx); assert_eq!( - view.read(app).text(app.as_ref()), - "aa…bbb\nccc…eeee\nggggg\n…i\njjjjj\nfffff" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(2, 2)..DisplayPoint::new(3, 2), - DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) - ] + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] ); - view.update(app, |view, ctx| view.move_line_down(&(), ctx)); + view.move_left(&(), ctx); assert_eq!( - view.read(app).text(app.as_ref()), - "ccc…eeee\naa…bbb\nfffff\nggggg\n…i\njjjjj" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(4, 2), - DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) - ] + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] ); - view.update(app, |view, ctx| view.move_line_down(&(), ctx)); + view.move_up(&(), ctx); assert_eq!( - view.read(app).text(app.as_ref()), - "ccc…eeee\nfffff\naa…bbb\nggggg\n…i\njjjjj" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(4, 2), - DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) - ] + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] ); - view.update(app, |view, ctx| view.move_line_up(&(), ctx)); + view.move_to_end(&(), ctx); assert_eq!( - view.read(app).text(app.as_ref()), - "ccc…eeee\naa…bbb\nggggg\n…i\njjjjj\nfffff" + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] ); + + view.move_to_beginning(&(), ctx); assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(2, 2)..DisplayPoint::new(3, 2), - DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) - ] + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)], ctx) + .unwrap(); + view.select_to_beginning(&(), ctx); + assert_eq!( + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] + ); + + view.select_to_end(&(), ctx); + assert_eq!( + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] ); }); } - #[test] - fn test_clipboard() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, "one two three four five six ", ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let view = app - .add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)) - .1; - - // Cut with three selections. Clipboard text is divided into three slices. - view.update(app, |view, ctx| { - view.select_ranges(vec![0..4, 8..14, 19..24], false, ctx); - view.cut(&(), ctx); - }); - assert_eq!(view.read(app).text(app.as_ref()), "two four six "); - - // Paste with three cursors. Each cursor pastes one slice of the clipboard text. - view.update(app, |view, ctx| { - view.select_ranges(vec![4..4, 9..9, 13..13], false, ctx); - view.paste(&(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "two one four three six five " - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), + #[gpui::test] + fn test_beginning_end_of_line(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "abc\n def", ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.select_display_ranges( &[ - DisplayPoint::new(0, 8)..DisplayPoint::new(0, 8), - DisplayPoint::new(0, 19)..DisplayPoint::new(0, 19), - DisplayPoint::new(0, 28)..DisplayPoint::new(0, 28) - ] - ); + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), + ], + ctx, + ) + .unwrap(); + }); - // Paste again but with only two cursors. Since the number of cursors doesn't - // match the number of slices in the clipboard, the entire clipboard text - // is pasted at each cursor. - view.update(app, |view, ctx| { - view.select_ranges(vec![0..0, 28..28], false, ctx); - view.insert(&"( ".to_string(), ctx); - view.paste(&(), ctx); - view.insert(&") ".to_string(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "( one three five ) two one four three six five ( one three five ) " - ); + view.update(app, |view, ctx| view.move_to_beginning_of_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + ] + ); - view.update(app, |view, ctx| { - view.select_ranges(vec![0..0], false, ctx); - view.insert(&"123\n4567\n89\n".to_string(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "123\n4567\n89\n( one three five ) two one four three six five ( one three five ) " - ); + view.update(app, |view, ctx| view.move_to_beginning_of_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); - // Cut with three selections, one of which is full-line. - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), - ], - ctx, - ) - .unwrap(); - view.cut(&(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "13\n9\n( one three five ) two one four three six five ( one three five ) " - ); + view.update(app, |view, ctx| view.move_to_beginning_of_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + ] + ); - // Paste with three selections, noticing how the copied selection that was full-line - // gets inserted before the second cursor. - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 2)..DisplayPoint::new(2, 3), - ], - ctx, - ) - .unwrap(); - view.paste(&(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "123\n4567\n9\n( 8ne three five ) two one four three six five ( one three five ) " - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), + view.update(app, |view, ctx| view.move_to_end_of_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + ] + ); + + // Moving to the end of line again is a no-op. + view.update(app, |view, ctx| view.move_to_end_of_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + ] + ); + + view.update(app, |view, ctx| { + view.move_left(&(), ctx); + view.select_to_beginning_of_line(&true, ctx); + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), + ] + ); + + view.update(app, |view, ctx| { + view.select_to_beginning_of_line(&true, ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0), + ] + ); + + view.update(app, |view, ctx| { + view.select_to_beginning_of_line(&true, ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), + ] + ); + + view.update(app, |view, ctx| view.select_to_end_of_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5), + ] + ); + + view.update(app, |view, ctx| view.delete_to_end_of_line(&(), ctx)); + assert_eq!(view.read(app).text(app.as_ref()), "ab\n de"); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), + ] + ); + + view.update(app, |view, ctx| view.delete_to_beginning_of_line(&(), ctx)); + assert_eq!(view.read(app).text(app.as_ref()), "\n"); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + } + + #[gpui::test] + fn test_prev_next_word_boundary(app: &mut gpui::MutableAppContext) { + let buffer = + app.add_model(|ctx| Buffer::new(0, "use std::str::{foo, bar}\n\n {baz.qux()}", ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.select_display_ranges( &[ + DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), + ], + ctx, + ) + .unwrap(); + }); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), + ] + ); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), + ] + ); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), + ] + ); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), + ] + ); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(0, 23)..DisplayPoint::new(0, 23), + ] + ); + + view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), + ] + ); + + view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + + view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), + ] + ); + + view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), + ] + ); + + view.update(app, |view, ctx| { + view.move_right(&(), ctx); + view.select_to_previous_word_boundary(&(), ctx); + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 2), + ] + ); + + view.update(app, |view, ctx| { + view.select_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 7), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 0), + ] + ); + + view.update(app, |view, ctx| view.select_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 2), + ] + ); + + view.update(app, |view, ctx| view.delete_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).text(app.as_ref()), + "use std::s::{foo, bar}\n\n {az.qux()}" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 10), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), + ] + ); + + view.update(app, |view, ctx| { + view.delete_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "use std::::{foo, bar}\n\n az.qux()}" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), + ] + ); + } + + #[gpui::test] + fn test_backspace(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| { + Buffer::new( + 0, + "one two three\nfour five six\nseven eight nine\nten\n", + ctx, + ) + }); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + // an empty selection - the preceding character is deleted DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3), - ] + // one character selected - it is deleted + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + // a line suffix selected - it is deleted + DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), + ], + ctx, + ) + .unwrap(); + view.backspace(&(), ctx); + }); + + assert_eq!( + buffer.read(app).text(), + "oe two three\nfou five six\nseven ten\n" + ); + } + + #[gpui::test] + fn test_delete(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| { + Buffer::new( + 0, + "one two three\nfour five six\nseven eight nine\nten\n", + ctx, + ) + }); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + // an empty selection - the following character is deleted + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + // one character selected - it is deleted + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + // a line suffix selected - it is deleted + DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), + ], + ctx, + ) + .unwrap(); + view.delete(&(), ctx); + }); + + assert_eq!( + buffer.read(app).text(), + "on two three\nfou five six\nseven ten\n" + ); + } + + #[gpui::test] + fn test_delete_line(app: &mut gpui::MutableAppContext) { + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ], + ctx, + ) + .unwrap(); + view.delete_line(&(), ctx); + }); + assert_eq!(view.read(app).text(app.as_ref()), "ghi"); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) + ] + ); + + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], ctx) + .unwrap(); + view.delete_line(&(), ctx); + }); + assert_eq!(view.read(app).text(app.as_ref()), "ghi\n"); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] + ); + } + + #[gpui::test] + fn test_duplicate_line(app: &mut gpui::MutableAppContext) { + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ], + ctx, + ) + .unwrap(); + view.duplicate_line(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "abc\nabc\ndef\ndef\nghi\n\n" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0), + ] + ); + + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1), + ], + ctx, + ) + .unwrap(); + view.duplicate_line(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "abc\ndef\nghi\nabc\ndef\nghi\n" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1), + DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1), + ] + ); + } + + #[gpui::test] + fn test_move_line_up_down(app: &mut gpui::MutableAppContext) { + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(10, 5), ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.fold_ranges( + vec![ + Point::new(0, 2)..Point::new(1, 2), + Point::new(2, 3)..Point::new(4, 1), + Point::new(7, 0)..Point::new(8, 4), + ], + ctx, ); + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 2), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2), + ], + ctx, + ) + .unwrap(); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "aa…bbb\nccc…eeee\nfffff\nggggg\n…i\njjjjj" + ); - // Copy with a single cursor only, which writes the whole line into the clipboard. - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)], - ctx, - ) - .unwrap(); - view.copy(&(), ctx); - }); + view.update(app, |view, ctx| view.move_line_up(&(), ctx)); + assert_eq!( + view.read(app).text(app.as_ref()), + "aa…bbb\nccc…eeee\nggggg\n…i\njjjjj\nfffff" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(3, 2), + DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) + ] + ); - // Paste with three selections, noticing how the copied full-line selection is inserted - // before the empty selections but replaces the selection that is non-empty. - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 2), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - ], - ctx, - ) + view.update(app, |view, ctx| view.move_line_down(&(), ctx)); + assert_eq!( + view.read(app).text(app.as_ref()), + "ccc…eeee\naa…bbb\nfffff\nggggg\n…i\njjjjj" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 2), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) + ] + ); + + view.update(app, |view, ctx| view.move_line_down(&(), ctx)); + assert_eq!( + view.read(app).text(app.as_ref()), + "ccc…eeee\nfffff\naa…bbb\nggggg\n…i\njjjjj" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 2), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) + ] + ); + + view.update(app, |view, ctx| view.move_line_up(&(), ctx)); + assert_eq!( + view.read(app).text(app.as_ref()), + "ccc…eeee\naa…bbb\nggggg\n…i\njjjjj\nfffff" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(3, 2), + DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) + ] + ); + } + + #[gpui::test] + fn test_clipboard(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "one two three four five six ", ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let view = app + .add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)) + .1; + + // Cut with three selections. Clipboard text is divided into three slices. + view.update(app, |view, ctx| { + view.select_ranges(vec![0..4, 8..14, 19..24], false, ctx); + view.cut(&(), ctx); + }); + assert_eq!(view.read(app).text(app.as_ref()), "two four six "); + + // Paste with three cursors. Each cursor pastes one slice of the clipboard text. + view.update(app, |view, ctx| { + view.select_ranges(vec![4..4, 9..9, 13..13], false, ctx); + view.paste(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "two one four three six five " + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 8)..DisplayPoint::new(0, 8), + DisplayPoint::new(0, 19)..DisplayPoint::new(0, 19), + DisplayPoint::new(0, 28)..DisplayPoint::new(0, 28) + ] + ); + + // Paste again but with only two cursors. Since the number of cursors doesn't + // match the number of slices in the clipboard, the entire clipboard text + // is pasted at each cursor. + view.update(app, |view, ctx| { + view.select_ranges(vec![0..0, 28..28], false, ctx); + view.insert(&"( ".to_string(), ctx); + view.paste(&(), ctx); + view.insert(&") ".to_string(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "( one three five ) two one four three six five ( one three five ) " + ); + + view.update(app, |view, ctx| { + view.select_ranges(vec![0..0], false, ctx); + view.insert(&"123\n4567\n89\n".to_string(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "123\n4567\n89\n( one three five ) two one four three six five ( one three five ) " + ); + + // Cut with three selections, one of which is full-line. + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), + ], + ctx, + ) + .unwrap(); + view.cut(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "13\n9\n( one three five ) two one four three six five ( one three five ) " + ); + + // Paste with three selections, noticing how the copied selection that was full-line + // gets inserted before the second cursor. + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 3), + ], + ctx, + ) + .unwrap(); + view.paste(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "123\n4567\n9\n( 8ne three five ) two one four three six five ( one three five ) " + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3), + ] + ); + + // Copy with a single cursor only, which writes the whole line into the clipboard. + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)], ctx) .unwrap(); - view.paste(&(), ctx); - }); - assert_eq!( + view.copy(&(), ctx); + }); + + // Paste with three selections, noticing how the copied full-line selection is inserted + // before the empty selections but replaces the selection that is non-empty. + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 2), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + ], + ctx, + ) + .unwrap(); + view.paste(&(), ctx); + }); + assert_eq!( view.read(app).text(app.as_ref()), "123\n123\n123\n67\n123\n9\n( 8ne three five ) two one four three six five ( one three five ) " ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(5, 1)..DisplayPoint::new(5, 1), + ] + ); + } + + #[gpui::test] + fn test_select_all(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "abc\nde\nfgh", ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |b, ctx| b.select_all(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)] + ); + } + + #[gpui::test] + fn test_select_line(app: &mut gpui::MutableAppContext) { + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 5), ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.select_display_ranges( &[ - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - DisplayPoint::new(5, 1)..DisplayPoint::new(5, 1), - ] - ); - }); - } - - #[test] - fn test_select_all() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\nde\nfgh", ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |b, ctx| b.select_all(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)] - ); - }); - } - - #[test] - fn test_select_line() { - App::test((), |app| { - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 5), ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), - ], - ctx, - ) - .unwrap(); - view.select_line(&(), ctx); - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0), - DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0), - ] - ); - - view.update(app, |view, ctx| view.select_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0), - DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5), - ] - ); - - view.update(app, |view, ctx| view.select_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] - ); - }); - } - - #[test] - fn test_split_selection_into_lines() { - App::test((), |app| { - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(9, 5), ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.fold_ranges( - vec![ - Point::new(0, 2)..Point::new(1, 2), - Point::new(2, 3)..Point::new(4, 1), - Point::new(7, 0)..Point::new(8, 4), - ], - ctx, - ); - view.select_display_ranges( - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), - ], - ctx, - ) - .unwrap(); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "aa…bbb\nccc…eeee\nfffff\nggggg\n…i" - ); - - view.update(app, |view, ctx| view.split_selection_into_lines(&(), ctx)); - assert_eq!( - view.read(app).text(app.as_ref()), - "aa…bbb\nccc…eeee\nfffff\nggggg\n…i" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2) - ] - ); - - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(4, 0)..DisplayPoint::new(0, 1)], - ctx, - ) - .unwrap(); - view.split_selection_into_lines(&(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\n…i" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), - DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), - DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5), - DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5), - DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5), - DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5), - DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0) - ] - ); + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), + ], + ctx, + ) + .unwrap(); + view.select_line(&(), ctx); }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0), + DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0), + ] + ); + + view.update(app, |view, ctx| view.select_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5), + ] + ); + + view.update(app, |view, ctx| view.select_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] + ); } - #[test] - fn test_add_selection_above_below() { - App::test((), |app| { - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndefghi\n\njk\nlmno\n", ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)], - ctx, - ) - .unwrap(); - }); - view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), + #[gpui::test] + fn test_split_selection_into_lines(app: &mut gpui::MutableAppContext) { + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(9, 5), ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.fold_ranges( vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) - ] - ); - - view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) - ] - ); - - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] - ); - - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) - ] - ); - - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) - ] - ); - - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)], - ctx, - ) - .unwrap(); - }); - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) - ] - ); - - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) - ] - ); - - view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] - ); - - view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] - ); - - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)], - ctx, - ) - .unwrap(); - }); - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), - ] - ); - - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), - DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4), - ] - ); - - view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), - ] - ); - - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)], - ctx, - ) - .unwrap(); - }); - view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), - ] - ); - - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), - ] + Point::new(0, 2)..Point::new(1, 2), + Point::new(2, 3)..Point::new(4, 1), + Point::new(7, 0)..Point::new(8, 4), + ], + ctx, ); + view.select_display_ranges( + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), + ], + ctx, + ) + .unwrap(); }); + assert_eq!( + view.read(app).text(app.as_ref()), + "aa…bbb\nccc…eeee\nfffff\nggggg\n…i" + ); + + view.update(app, |view, ctx| view.split_selection_into_lines(&(), ctx)); + assert_eq!( + view.read(app).text(app.as_ref()), + "aa…bbb\nccc…eeee\nfffff\nggggg\n…i" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2) + ] + ); + + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(4, 0)..DisplayPoint::new(0, 1)], ctx) + .unwrap(); + view.split_selection_into_lines(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\n…i" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), + DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5), + DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5), + DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5), + DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5), + DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0) + ] + ); + } + + #[gpui::test] + fn test_add_selection_above_below(app: &mut gpui::MutableAppContext) { + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndefghi\n\njk\nlmno\n", ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)], ctx) + .unwrap(); + }); + view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + + view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] + ); + + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) + ] + ); + + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) + ] + ); + + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)], ctx) + .unwrap(); + }); + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) + ] + ); + + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) + ] + ); + + view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] + ); + + view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] + ); + + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)], ctx) + .unwrap(); + }); + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + ] + ); + + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4), + ] + ); + + view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + ] + ); + + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)], ctx) + .unwrap(); + }); + view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), + ] + ); + + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), + ] + ); } impl BufferView { diff --git a/zed/src/editor/display_map/fold_map.rs b/zed/src/editor/display_map/fold_map.rs index e7f538c686..e33013565f 100644 --- a/zed/src/editor/display_map/fold_map.rs +++ b/zed/src/editor/display_map/fold_map.rs @@ -671,176 +671,163 @@ mod tests { use super::*; use crate::test::sample_text; use buffer::ToPoint; - use gpui::App; - #[test] - fn test_basic_folds() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); - let mut map = FoldMap::new(buffer.clone(), app.as_ref()); + #[gpui::test] + fn test_basic_folds(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); + let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - map.fold( - vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(2, 4)..Point::new(4, 1), - ], - app.as_ref(), - ) - .unwrap(); - assert_eq!(map.text(app.as_ref()), "aa…cc…eeeee"); + map.fold( + vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(2, 4)..Point::new(4, 1), + ], + app.as_ref(), + ) + .unwrap(); + assert_eq!(map.text(app.as_ref()), "aa…cc…eeeee"); - buffer.update(app, |buffer, ctx| { - buffer - .edit( - vec![ - Point::new(0, 0)..Point::new(0, 1), - Point::new(2, 3)..Point::new(2, 3), - ], - "123", - Some(ctx), - ) - .unwrap(); - }); - assert_eq!(map.text(app.as_ref()), "123a…c123c…eeeee"); - - buffer.update(app, |buffer, ctx| { - let start_version = buffer.version.clone(); - buffer - .edit(Some(Point::new(2, 6)..Point::new(4, 3)), "456", Some(ctx)) - .unwrap(); - buffer.edits_since(start_version).collect::>() - }); - assert_eq!(map.text(app.as_ref()), "123a…c123456eee"); - - map.unfold(Some(Point::new(0, 4)..Point::new(0, 5)), app.as_ref()) + buffer.update(app, |buffer, ctx| { + buffer + .edit( + vec![ + Point::new(0, 0)..Point::new(0, 1), + Point::new(2, 3)..Point::new(2, 3), + ], + "123", + Some(ctx), + ) .unwrap(); - assert_eq!(map.text(app.as_ref()), "123aaaaa\nbbbbbb\nccc123456eee"); }); - } + assert_eq!(map.text(app.as_ref()), "123a…c123c…eeeee"); - #[test] - fn test_adjacent_folds() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, "abcdefghijkl", ctx)); - - { - let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - - map.fold(vec![5..8], app.as_ref()).unwrap(); - map.check_invariants(app.as_ref()); - assert_eq!(map.text(app.as_ref()), "abcde…ijkl"); - - // Create an fold adjacent to the start of the first fold. - map.fold(vec![0..1, 2..5], app.as_ref()).unwrap(); - map.check_invariants(app.as_ref()); - assert_eq!(map.text(app.as_ref()), "…b…ijkl"); - - // Create an fold adjacent to the end of the first fold. - map.fold(vec![11..11, 8..10], app.as_ref()).unwrap(); - map.check_invariants(app.as_ref()); - assert_eq!(map.text(app.as_ref()), "…b…kl"); - } - - { - let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - - // Create two adjacent folds. - map.fold(vec![0..2, 2..5], app.as_ref()).unwrap(); - map.check_invariants(app.as_ref()); - assert_eq!(map.text(app.as_ref()), "…fghijkl"); - - // Edit within one of the folds. - buffer.update(app, |buffer, ctx| { - let version = buffer.version(); - buffer.edit(vec![0..1], "12345", Some(ctx)).unwrap(); - buffer.edits_since(version).collect::>() - }); - map.check_invariants(app.as_ref()); - assert_eq!(map.text(app.as_ref()), "12345…fghijkl"); - } + buffer.update(app, |buffer, ctx| { + let start_version = buffer.version.clone(); + buffer + .edit(Some(Point::new(2, 6)..Point::new(4, 3)), "456", Some(ctx)) + .unwrap(); + buffer.edits_since(start_version).collect::>() }); - } + assert_eq!(map.text(app.as_ref()), "123a…c123456eee"); - #[test] - fn test_overlapping_folds() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); - let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - map.fold( - vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(0, 4)..Point::new(1, 0), - Point::new(1, 2)..Point::new(3, 2), - Point::new(3, 1)..Point::new(4, 1), - ], - app.as_ref(), - ) + map.unfold(Some(Point::new(0, 4)..Point::new(0, 5)), app.as_ref()) .unwrap(); - assert_eq!(map.text(app.as_ref()), "aa…eeeee"); - }) + assert_eq!(map.text(app.as_ref()), "123aaaaa\nbbbbbb\nccc123456eee"); } - #[test] - fn test_merging_folds_via_edit() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); + #[gpui::test] + fn test_adjacent_folds(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "abcdefghijkl", ctx)); + + { let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - map.fold( - vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(3, 1)..Point::new(4, 1), - ], - app.as_ref(), - ) - .unwrap(); - assert_eq!(map.text(app.as_ref()), "aa…cccc\nd…eeeee"); + map.fold(vec![5..8], app.as_ref()).unwrap(); + map.check_invariants(app.as_ref()); + assert_eq!(map.text(app.as_ref()), "abcde…ijkl"); + // Create an fold adjacent to the start of the first fold. + map.fold(vec![0..1, 2..5], app.as_ref()).unwrap(); + map.check_invariants(app.as_ref()); + assert_eq!(map.text(app.as_ref()), "…b…ijkl"); + + // Create an fold adjacent to the end of the first fold. + map.fold(vec![11..11, 8..10], app.as_ref()).unwrap(); + map.check_invariants(app.as_ref()); + assert_eq!(map.text(app.as_ref()), "…b…kl"); + } + + { + let mut map = FoldMap::new(buffer.clone(), app.as_ref()); + + // Create two adjacent folds. + map.fold(vec![0..2, 2..5], app.as_ref()).unwrap(); + map.check_invariants(app.as_ref()); + assert_eq!(map.text(app.as_ref()), "…fghijkl"); + + // Edit within one of the folds. buffer.update(app, |buffer, ctx| { - buffer - .edit(Some(Point::new(2, 2)..Point::new(3, 1)), "", Some(ctx)) - .unwrap(); + let version = buffer.version(); + buffer.edit(vec![0..1], "12345", Some(ctx)).unwrap(); + buffer.edits_since(version).collect::>() }); - assert_eq!(map.text(app.as_ref()), "aa…eeeee"); - }); + map.check_invariants(app.as_ref()); + assert_eq!(map.text(app.as_ref()), "12345…fghijkl"); + } } - #[test] - fn test_folds_in_range() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); - let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - let buffer = buffer.read(app); - - map.fold( - vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(0, 4)..Point::new(1, 0), - Point::new(1, 2)..Point::new(3, 2), - Point::new(3, 1)..Point::new(4, 1), - ], - app.as_ref(), - ) - .unwrap(); - let fold_ranges = map - .folds_in_range(Point::new(1, 0)..Point::new(1, 3), app.as_ref()) - .unwrap() - .map(|fold| { - fold.start.to_point(buffer).unwrap()..fold.end.to_point(buffer).unwrap() - }) - .collect::>(); - assert_eq!( - fold_ranges, - vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(1, 2)..Point::new(3, 2) - ] - ); - }); + #[gpui::test] + fn test_overlapping_folds(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); + let mut map = FoldMap::new(buffer.clone(), app.as_ref()); + map.fold( + vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(0, 4)..Point::new(1, 0), + Point::new(1, 2)..Point::new(3, 2), + Point::new(3, 1)..Point::new(4, 1), + ], + app.as_ref(), + ) + .unwrap(); + assert_eq!(map.text(app.as_ref()), "aa…eeeee"); } - #[test] - fn test_random_folds() { + #[gpui::test] + fn test_merging_folds_via_edit(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); + let mut map = FoldMap::new(buffer.clone(), app.as_ref()); + + map.fold( + vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(3, 1)..Point::new(4, 1), + ], + app.as_ref(), + ) + .unwrap(); + assert_eq!(map.text(app.as_ref()), "aa…cccc\nd…eeeee"); + + buffer.update(app, |buffer, ctx| { + buffer + .edit(Some(Point::new(2, 2)..Point::new(3, 1)), "", Some(ctx)) + .unwrap(); + }); + assert_eq!(map.text(app.as_ref()), "aa…eeeee"); + } + + #[gpui::test] + fn test_folds_in_range(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); + let mut map = FoldMap::new(buffer.clone(), app.as_ref()); + let buffer = buffer.read(app); + + map.fold( + vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(0, 4)..Point::new(1, 0), + Point::new(1, 2)..Point::new(3, 2), + Point::new(3, 1)..Point::new(4, 1), + ], + app.as_ref(), + ) + .unwrap(); + let fold_ranges = map + .folds_in_range(Point::new(1, 0)..Point::new(1, 3), app.as_ref()) + .unwrap() + .map(|fold| fold.start.to_point(buffer).unwrap()..fold.end.to_point(buffer).unwrap()) + .collect::>(); + assert_eq!( + fold_ranges, + vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(1, 2)..Point::new(3, 2) + ] + ); + } + + #[gpui::test] + fn test_random_folds(app: &mut gpui::MutableAppContext) { use crate::editor::ToPoint; use crate::util::RandomCharIter; use rand::prelude::*; @@ -863,203 +850,197 @@ mod tests { dbg!(seed); let mut rng = StdRng::seed_from_u64(seed); - App::test((), |app| { - let buffer = app.add_model(|ctx| { - let len = rng.gen_range(0..10); - let text = RandomCharIter::new(&mut rng).take(len).collect::(); - Buffer::new(0, text, ctx) - }); - let mut map = FoldMap::new(buffer.clone(), app.as_ref()); + let buffer = app.add_model(|ctx| { + let len = rng.gen_range(0..10); + let text = RandomCharIter::new(&mut rng).take(len).collect::(); + Buffer::new(0, text, ctx) + }); + let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - for _ in 0..operations { - log::info!("text: {:?}", buffer.read(app).text()); - match rng.gen_range(0..=100) { - 0..=34 => { - let buffer = buffer.read(app); - let mut to_fold = Vec::new(); - for _ in 0..rng.gen_range(1..=5) { - let end = rng.gen_range(0..=buffer.len()); - let start = rng.gen_range(0..=end); - to_fold.push(start..end); - } - log::info!("folding {:?}", to_fold); - map.fold(to_fold, app.as_ref()).unwrap(); + for _ in 0..operations { + log::info!("text: {:?}", buffer.read(app).text()); + match rng.gen_range(0..=100) { + 0..=34 => { + let buffer = buffer.read(app); + let mut to_fold = Vec::new(); + for _ in 0..rng.gen_range(1..=5) { + let end = rng.gen_range(0..=buffer.len()); + let start = rng.gen_range(0..=end); + to_fold.push(start..end); } - 35..=59 if !map.folds.is_empty() => { - let buffer = buffer.read(app); - let mut to_unfold = Vec::new(); - for _ in 0..rng.gen_range(1..=3) { - let end = rng.gen_range(0..=buffer.len()); - let start = rng.gen_range(0..=end); - to_unfold.push(start..end); - } - log::info!("unfolding {:?}", to_unfold); - map.unfold(to_unfold, app.as_ref()).unwrap(); - } - _ => { - let edits = buffer.update(app, |buffer, ctx| { - let start_version = buffer.version.clone(); - let edit_count = rng.gen_range(1..=5); - buffer.randomly_edit(&mut rng, edit_count, Some(ctx)); - buffer.edits_since(start_version).collect::>() - }); - log::info!("editing {:?}", edits); + log::info!("folding {:?}", to_fold); + map.fold(to_fold, app.as_ref()).unwrap(); + } + 35..=59 if !map.folds.is_empty() => { + let buffer = buffer.read(app); + let mut to_unfold = Vec::new(); + for _ in 0..rng.gen_range(1..=3) { + let end = rng.gen_range(0..=buffer.len()); + let start = rng.gen_range(0..=end); + to_unfold.push(start..end); } + log::info!("unfolding {:?}", to_unfold); + map.unfold(to_unfold, app.as_ref()).unwrap(); } - map.check_invariants(app.as_ref()); - - let buffer = map.buffer.read(app); - let mut expected_text = buffer.text(); - let mut expected_buffer_rows = Vec::new(); - let mut next_row = buffer.max_point().row; - for fold_range in map.merged_fold_ranges(app.as_ref()).into_iter().rev() { - let fold_start = buffer.point_for_offset(fold_range.start).unwrap(); - let fold_end = buffer.point_for_offset(fold_range.end).unwrap(); - expected_buffer_rows.extend((fold_end.row + 1..=next_row).rev()); - next_row = fold_start.row; - - expected_text.replace_range(fold_range.start..fold_range.end, "…"); - } - expected_buffer_rows.extend((0..=next_row).rev()); - expected_buffer_rows.reverse(); - - assert_eq!(map.text(app.as_ref()), expected_text); - - for (display_row, line) in expected_text.lines().enumerate() { - let line_len = map.line_len(display_row as u32, app.as_ref()).unwrap(); - assert_eq!(line_len, line.chars().count() as u32); - } - - let mut display_point = DisplayPoint::new(0, 0); - let mut display_offset = DisplayOffset(0); - for c in expected_text.chars() { - let buffer_point = map.to_buffer_point(display_point, app.as_ref()); - let buffer_offset = buffer_point.to_offset(buffer).unwrap(); - assert_eq!( - map.to_display_point(buffer_point, app.as_ref()), - display_point - ); - assert_eq!( - map.to_buffer_offset(display_point, app.as_ref()).unwrap(), - buffer_offset - ); - assert_eq!( - map.to_display_offset(display_point, app.as_ref()).unwrap(), - display_offset - ); - - if c == '\n' { - *display_point.row_mut() += 1; - *display_point.column_mut() = 0; - } else { - *display_point.column_mut() += 1; - } - display_offset.0 += 1; - } - - for _ in 0..5 { - let row = rng.gen_range(0..=map.max_point(app.as_ref()).row()); - let column = rng.gen_range(0..=map.line_len(row, app.as_ref()).unwrap()); - let point = DisplayPoint::new(row, column); - let offset = map.to_display_offset(point, app.as_ref()).unwrap().0; - let len = rng.gen_range(0..=map.len(app.as_ref()) - offset); - assert_eq!( - map.snapshot(app.as_ref()) - .chars_at(point, app.as_ref()) - .unwrap() - .take(len) - .collect::(), - expected_text - .chars() - .skip(offset) - .take(len) - .collect::() - ); - } - - for (idx, buffer_row) in expected_buffer_rows.iter().enumerate() { - let display_row = map - .to_display_point(Point::new(*buffer_row, 0), app.as_ref()) - .row(); - assert_eq!( - map.snapshot(app.as_ref()) - .buffer_rows(display_row) - .unwrap() - .collect::>(), - expected_buffer_rows[idx..], - ); - } - - for fold_range in map.merged_fold_ranges(app.as_ref()) { - let display_point = map.to_display_point( - fold_range.start.to_point(buffer).unwrap(), - app.as_ref(), - ); - assert!(map.is_line_folded(display_point.row(), app.as_ref())); - } - - for _ in 0..5 { - let end = rng.gen_range(0..=buffer.len()); - let start = rng.gen_range(0..=end); - let expected_folds = map - .folds - .items() - .into_iter() - .filter(|fold| { - let start = buffer.anchor_before(start).unwrap(); - let end = buffer.anchor_after(end).unwrap(); - start.cmp(&fold.0.end, buffer).unwrap() == Ordering::Less - && end.cmp(&fold.0.start, buffer).unwrap() == Ordering::Greater - }) - .map(|fold| fold.0) - .collect::>(); - - assert_eq!( - map.folds_in_range(start..end, app.as_ref()) - .unwrap() - .cloned() - .collect::>(), - expected_folds - ); + _ => { + let edits = buffer.update(app, |buffer, ctx| { + let start_version = buffer.version.clone(); + let edit_count = rng.gen_range(1..=5); + buffer.randomly_edit(&mut rng, edit_count, Some(ctx)); + buffer.edits_since(start_version).collect::>() + }); + log::info!("editing {:?}", edits); } } - }); + map.check_invariants(app.as_ref()); + + let buffer = map.buffer.read(app); + let mut expected_text = buffer.text(); + let mut expected_buffer_rows = Vec::new(); + let mut next_row = buffer.max_point().row; + for fold_range in map.merged_fold_ranges(app.as_ref()).into_iter().rev() { + let fold_start = buffer.point_for_offset(fold_range.start).unwrap(); + let fold_end = buffer.point_for_offset(fold_range.end).unwrap(); + expected_buffer_rows.extend((fold_end.row + 1..=next_row).rev()); + next_row = fold_start.row; + + expected_text.replace_range(fold_range.start..fold_range.end, "…"); + } + expected_buffer_rows.extend((0..=next_row).rev()); + expected_buffer_rows.reverse(); + + assert_eq!(map.text(app.as_ref()), expected_text); + + for (display_row, line) in expected_text.lines().enumerate() { + let line_len = map.line_len(display_row as u32, app.as_ref()).unwrap(); + assert_eq!(line_len, line.chars().count() as u32); + } + + let mut display_point = DisplayPoint::new(0, 0); + let mut display_offset = DisplayOffset(0); + for c in expected_text.chars() { + let buffer_point = map.to_buffer_point(display_point, app.as_ref()); + let buffer_offset = buffer_point.to_offset(buffer).unwrap(); + assert_eq!( + map.to_display_point(buffer_point, app.as_ref()), + display_point + ); + assert_eq!( + map.to_buffer_offset(display_point, app.as_ref()).unwrap(), + buffer_offset + ); + assert_eq!( + map.to_display_offset(display_point, app.as_ref()).unwrap(), + display_offset + ); + + if c == '\n' { + *display_point.row_mut() += 1; + *display_point.column_mut() = 0; + } else { + *display_point.column_mut() += 1; + } + display_offset.0 += 1; + } + + for _ in 0..5 { + let row = rng.gen_range(0..=map.max_point(app.as_ref()).row()); + let column = rng.gen_range(0..=map.line_len(row, app.as_ref()).unwrap()); + let point = DisplayPoint::new(row, column); + let offset = map.to_display_offset(point, app.as_ref()).unwrap().0; + let len = rng.gen_range(0..=map.len(app.as_ref()) - offset); + assert_eq!( + map.snapshot(app.as_ref()) + .chars_at(point, app.as_ref()) + .unwrap() + .take(len) + .collect::(), + expected_text + .chars() + .skip(offset) + .take(len) + .collect::() + ); + } + + for (idx, buffer_row) in expected_buffer_rows.iter().enumerate() { + let display_row = map + .to_display_point(Point::new(*buffer_row, 0), app.as_ref()) + .row(); + assert_eq!( + map.snapshot(app.as_ref()) + .buffer_rows(display_row) + .unwrap() + .collect::>(), + expected_buffer_rows[idx..], + ); + } + + for fold_range in map.merged_fold_ranges(app.as_ref()) { + let display_point = map + .to_display_point(fold_range.start.to_point(buffer).unwrap(), app.as_ref()); + assert!(map.is_line_folded(display_point.row(), app.as_ref())); + } + + for _ in 0..5 { + let end = rng.gen_range(0..=buffer.len()); + let start = rng.gen_range(0..=end); + let expected_folds = map + .folds + .items() + .into_iter() + .filter(|fold| { + let start = buffer.anchor_before(start).unwrap(); + let end = buffer.anchor_after(end).unwrap(); + start.cmp(&fold.0.end, buffer).unwrap() == Ordering::Less + && end.cmp(&fold.0.start, buffer).unwrap() == Ordering::Greater + }) + .map(|fold| fold.0) + .collect::>(); + + assert_eq!( + map.folds_in_range(start..end, app.as_ref()) + .unwrap() + .cloned() + .collect::>(), + expected_folds + ); + } + } } } - #[test] - fn test_buffer_rows() { - App::test((), |app| { - let text = sample_text(6, 6) + "\n"; - let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx)); + #[gpui::test] + fn test_buffer_rows(app: &mut gpui::MutableAppContext) { + let text = sample_text(6, 6) + "\n"; + let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx)); - let mut map = FoldMap::new(buffer.clone(), app.as_ref()); + let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - map.fold( - vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(3, 1)..Point::new(4, 1), - ], - app.as_ref(), - ) - .unwrap(); + map.fold( + vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(3, 1)..Point::new(4, 1), + ], + app.as_ref(), + ) + .unwrap(); - assert_eq!(map.text(app.as_ref()), "aa…cccc\nd…eeeee\nffffff\n"); - assert_eq!( - map.snapshot(app.as_ref()) - .buffer_rows(0) - .unwrap() - .collect::>(), - vec![0, 3, 5, 6] - ); - assert_eq!( - map.snapshot(app.as_ref()) - .buffer_rows(3) - .unwrap() - .collect::>(), - vec![6] - ); - }); + assert_eq!(map.text(app.as_ref()), "aa…cccc\nd…eeeee\nffffff\n"); + assert_eq!( + map.snapshot(app.as_ref()) + .buffer_rows(0) + .unwrap() + .collect::>(), + vec![0, 3, 5, 6] + ); + assert_eq!( + map.snapshot(app.as_ref()) + .buffer_rows(3) + .unwrap() + .collect::>(), + vec![6] + ); } impl FoldMap { diff --git a/zed/src/editor/display_map/mod.rs b/zed/src/editor/display_map/mod.rs index 97c1c0891b..608d39dac8 100644 --- a/zed/src/editor/display_map/mod.rs +++ b/zed/src/editor/display_map/mod.rs @@ -339,53 +339,50 @@ pub fn collapse_tabs( mod tests { use super::*; use crate::test::*; - use gpui::App; - #[test] - fn test_chars_at() { - App::test((), |app| { - let text = sample_text(6, 6); - let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx)); - let map = DisplayMap::new(buffer.clone(), 4, app.as_ref()); - buffer - .update(app, |buffer, ctx| { - buffer.edit( - vec![ - Point::new(1, 0)..Point::new(1, 0), - Point::new(1, 1)..Point::new(1, 1), - Point::new(2, 1)..Point::new(2, 1), - ], - "\t", - Some(ctx), - ) - }) - .unwrap(); + #[gpui::test] + fn test_chars_at(app: &mut gpui::MutableAppContext) { + let text = sample_text(6, 6); + let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx)); + let map = DisplayMap::new(buffer.clone(), 4, app.as_ref()); + buffer + .update(app, |buffer, ctx| { + buffer.edit( + vec![ + Point::new(1, 0)..Point::new(1, 0), + Point::new(1, 1)..Point::new(1, 1), + Point::new(2, 1)..Point::new(2, 1), + ], + "\t", + Some(ctx), + ) + }) + .unwrap(); - assert_eq!( - map.snapshot(app.as_ref()) - .chars_at(DisplayPoint::new(1, 0), app.as_ref()) - .unwrap() - .take(10) - .collect::(), - " b bb" - ); - assert_eq!( - map.snapshot(app.as_ref()) - .chars_at(DisplayPoint::new(1, 2), app.as_ref()) - .unwrap() - .take(10) - .collect::(), - " b bbbb" - ); - assert_eq!( - map.snapshot(app.as_ref()) - .chars_at(DisplayPoint::new(1, 6), app.as_ref()) - .unwrap() - .take(13) - .collect::(), - " bbbbb\nc c" - ); - }); + assert_eq!( + map.snapshot(app.as_ref()) + .chars_at(DisplayPoint::new(1, 0), app.as_ref()) + .unwrap() + .take(10) + .collect::(), + " b bb" + ); + assert_eq!( + map.snapshot(app.as_ref()) + .chars_at(DisplayPoint::new(1, 2), app.as_ref()) + .unwrap() + .take(10) + .collect::(), + " b bbbb" + ); + assert_eq!( + map.snapshot(app.as_ref()) + .chars_at(DisplayPoint::new(1, 6), app.as_ref()) + .unwrap() + .take(13) + .collect::(), + " bbbbb\nc c" + ); } #[test] @@ -411,12 +408,10 @@ mod tests { assert_eq!(collapse_tabs("\ta".chars(), 5, Bias::Right, 4), (2, 0)); } - #[test] - fn test_max_point() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, "aaa\n\t\tbbb", ctx)); - let map = DisplayMap::new(buffer.clone(), 4, app.as_ref()); - assert_eq!(map.max_point(app.as_ref()), DisplayPoint::new(1, 11)) - }); + #[gpui::test] + fn test_max_point(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "aaa\n\t\tbbb", ctx)); + let map = DisplayMap::new(buffer.clone(), 4, app.as_ref()); + assert_eq!(map.max_point(app.as_ref()), DisplayPoint::new(1, 11)) } } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 7253e0a5af..1b25473173 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -734,65 +734,63 @@ mod tests { use std::collections::HashSet; use tempdir::TempDir; - #[test] - fn test_open_paths_action() { - App::test((), |app| { - let settings = settings::channel(&app.font_cache()).unwrap().1; + #[gpui::test] + fn test_open_paths_action(app: &mut gpui::MutableAppContext) { + let settings = settings::channel(&app.font_cache()).unwrap().1; - init(app); + init(app); - let dir = temp_tree(json!({ - "a": { - "aa": null, - "ab": null, - }, - "b": { - "ba": null, - "bb": null, - }, - "c": { - "ca": null, - "cb": null, - }, - })); + let dir = temp_tree(json!({ + "a": { + "aa": null, + "ab": null, + }, + "b": { + "ba": null, + "bb": null, + }, + "c": { + "ca": null, + "cb": null, + }, + })); - app.dispatch_global_action( - "workspace:open_paths", - OpenParams { - paths: vec![ - dir.path().join("a").to_path_buf(), - dir.path().join("b").to_path_buf(), - ], - settings: settings.clone(), - }, - ); - assert_eq!(app.window_ids().count(), 1); + app.dispatch_global_action( + "workspace:open_paths", + OpenParams { + paths: vec![ + dir.path().join("a").to_path_buf(), + dir.path().join("b").to_path_buf(), + ], + settings: settings.clone(), + }, + ); + assert_eq!(app.window_ids().count(), 1); - app.dispatch_global_action( - "workspace:open_paths", - OpenParams { - paths: vec![dir.path().join("a").to_path_buf()], - settings: settings.clone(), - }, - ); - assert_eq!(app.window_ids().count(), 1); - let workspace_view_1 = app - .root_view::(app.window_ids().next().unwrap()) - .unwrap(); - assert_eq!(workspace_view_1.read(app).worktrees().len(), 2); + app.dispatch_global_action( + "workspace:open_paths", + OpenParams { + paths: vec![dir.path().join("a").to_path_buf()], + settings: settings.clone(), + }, + ); + assert_eq!(app.window_ids().count(), 1); + let workspace_view_1 = app + .root_view::(app.window_ids().next().unwrap()) + .unwrap(); + assert_eq!(workspace_view_1.read(app).worktrees().len(), 2); - app.dispatch_global_action( - "workspace:open_paths", - OpenParams { - paths: vec![ - dir.path().join("b").to_path_buf(), - dir.path().join("c").to_path_buf(), - ], - settings: settings.clone(), - }, - ); - assert_eq!(app.window_ids().count(), 2); - }); + app.dispatch_global_action( + "workspace:open_paths", + OpenParams { + paths: vec![ + dir.path().join("b").to_path_buf(), + dir.path().join("c").to_path_buf(), + ], + settings: settings.clone(), + }, + ); + assert_eq!(app.window_ids().count(), 2); } #[test] From ab04d715083a3db6a929353b74dd7408ee59ddf8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 12 May 2021 18:20:49 +0200 Subject: [PATCH 3/7] Use `gpui::test` in async tests in zed Co-Authored-By: Max Brunsfeld --- zed/src/file_finder.rs | 396 +++++++++++++-------------- zed/src/workspace.rs | 595 ++++++++++++++++++++--------------------- zed/src/worktree.rs | 424 ++++++++++++++--------------- 3 files changed, 691 insertions(+), 724 deletions(-) diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 0f966cc578..e2427adddd 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -453,220 +453,208 @@ impl FileFinder { mod tests { use super::*; use crate::{editor, settings, test::temp_tree, workspace::Workspace}; - use gpui::App; use serde_json::json; use std::fs; use tempdir::TempDir; - #[test] - fn test_matching_paths() { - App::test_async((), |mut app| async move { - let tmp_dir = TempDir::new("example").unwrap(); - fs::create_dir(tmp_dir.path().join("a")).unwrap(); - fs::write(tmp_dir.path().join("a/banana"), "banana").unwrap(); - fs::write(tmp_dir.path().join("a/bandana"), "bandana").unwrap(); - app.update(|ctx| { - super::init(ctx); - editor::init(ctx); - }); - - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (window_id, workspace) = app.add_window(|ctx| { - let mut workspace = Workspace::new(0, settings, ctx); - workspace.add_worktree(tmp_dir.path(), ctx); - workspace - }); - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; - app.dispatch_action( - window_id, - vec![workspace.id()], - "file_finder:toggle".into(), - (), - ); - - let finder = app.read(|ctx| { - workspace - .read(ctx) - .modal() - .cloned() - .unwrap() - .downcast::() - .unwrap() - }); - let query_buffer = app.read(|ctx| finder.read(ctx).query_buffer.clone()); - - let chain = vec![finder.id(), query_buffer.id()]; - app.dispatch_action(window_id, chain.clone(), "buffer:insert", "b".to_string()); - app.dispatch_action(window_id, chain.clone(), "buffer:insert", "n".to_string()); - app.dispatch_action(window_id, chain.clone(), "buffer:insert", "a".to_string()); - finder - .condition(&app, |finder, _| finder.matches.len() == 2) - .await; - - let active_pane = app.read(|ctx| workspace.read(ctx).active_pane().clone()); - app.dispatch_action( - window_id, - vec![workspace.id(), finder.id()], - "menu:select_next", - (), - ); - app.dispatch_action( - window_id, - vec![workspace.id(), finder.id()], - "file_finder:confirm", - (), - ); - active_pane - .condition(&app, |pane, _| pane.active_item().is_some()) - .await; - app.read(|ctx| { - let active_item = active_pane.read(ctx).active_item().unwrap(); - assert_eq!(active_item.title(ctx), "bandana"); - }); + #[gpui::test] + async fn test_matching_paths(mut app: gpui::TestAppContext) { + let tmp_dir = TempDir::new("example").unwrap(); + fs::create_dir(tmp_dir.path().join("a")).unwrap(); + fs::write(tmp_dir.path().join("a/banana"), "banana").unwrap(); + fs::write(tmp_dir.path().join("a/bandana"), "bandana").unwrap(); + app.update(|ctx| { + super::init(ctx); + editor::init(ctx); }); - } - - #[test] - fn test_matching_cancellation() { - App::test_async((), |mut app| async move { - let tmp_dir = temp_tree(json!({ - "hello": "", - "goodbye": "", - "halogen-light": "", - "happiness": "", - "height": "", - "hi": "", - "hiccup": "", - })); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, workspace) = app.add_window(|ctx| { - let mut workspace = Workspace::new(0, settings.clone(), ctx); - workspace.add_worktree(tmp_dir.path(), ctx); - workspace - }); - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; - let (_, finder) = - app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx)); - - let query = "hi".to_string(); - finder.update(&mut app, |f, ctx| f.spawn_search(query.clone(), ctx)); - finder.condition(&app, |f, _| f.matches.len() == 5).await; - - finder.update(&mut app, |finder, ctx| { - let matches = finder.matches.clone(); - - // Simulate a search being cancelled after the time limit, - // returning only a subset of the matches that would have been found. - finder.spawn_search(query.clone(), ctx); - finder.update_matches( - ( - finder.latest_search_id, - true, // did-cancel - query.clone(), - vec![matches[1].clone(), matches[3].clone()], - ), - ctx, - ); - - // Simulate another cancellation. - finder.spawn_search(query.clone(), ctx); - finder.update_matches( - ( - finder.latest_search_id, - true, // did-cancel - query.clone(), - vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], - ), - ctx, - ); - - assert_eq!(finder.matches, matches[0..4]) - }); - }); - } - - #[test] - fn test_single_file_worktrees() { - App::test_async((), |mut app| async move { - let temp_dir = TempDir::new("test-single-file-worktrees").unwrap(); - let dir_path = temp_dir.path().join("the-parent-dir"); - let file_path = dir_path.join("the-file"); - fs::create_dir(&dir_path).unwrap(); - fs::write(&file_path, "").unwrap(); - - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, workspace) = app.add_window(|ctx| { - let mut workspace = Workspace::new(0, settings.clone(), ctx); - workspace.add_worktree(&file_path, ctx); - workspace - }); - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; - let (_, finder) = - app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx)); - - // Even though there is only one worktree, that worktree's filename - // is included in the matching, because the worktree is a single file. - finder.update(&mut app, |f, ctx| f.spawn_search("thf".into(), ctx)); - finder.condition(&app, |f, _| f.matches.len() == 1).await; - - app.read(|ctx| { - let finder = finder.read(ctx); - let (file_name, file_name_positions, full_path, full_path_positions) = - finder.labels_for_match(&finder.matches[0], ctx).unwrap(); - - assert_eq!(file_name, "the-file"); - assert_eq!(file_name_positions, &[0, 1, 4]); - assert_eq!(full_path, "the-file"); - assert_eq!(full_path_positions, &[0, 1, 4]); - }); - - // Since the worktree root is a file, searching for its name followed by a slash does - // not match anything. - finder.update(&mut app, |f, ctx| f.spawn_search("thf/".into(), ctx)); - finder.condition(&app, |f, _| f.matches.len() == 0).await; - }); - } - - #[test] - fn test_multiple_matches_with_same_relative_path() { - App::test_async((), |mut app| async move { - let tmp_dir = temp_tree(json!({ - "dir1": { "a.txt": "" }, - "dir2": { "a.txt": "" } - })); - let settings = settings::channel(&app.font_cache()).unwrap().1; - - let (_, workspace) = app.add_window(|ctx| Workspace::new(0, settings.clone(), ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (window_id, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(tmp_dir.path(), ctx); workspace - .update(&mut app, |workspace, ctx| { - workspace.open_paths( - &[tmp_dir.path().join("dir1"), tmp_dir.path().join("dir2")], - ctx, - ) - }) - .await; - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; + }); + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; + app.dispatch_action( + window_id, + vec![workspace.id()], + "file_finder:toggle".into(), + (), + ); - let (_, finder) = - app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx)); + let finder = app.read(|ctx| { + workspace + .read(ctx) + .modal() + .cloned() + .unwrap() + .downcast::() + .unwrap() + }); + let query_buffer = app.read(|ctx| finder.read(ctx).query_buffer.clone()); - // Run a search that matches two files with the same relative path. - finder.update(&mut app, |f, ctx| f.spawn_search("a.t".into(), ctx)); - finder.condition(&app, |f, _| f.matches.len() == 2).await; + let chain = vec![finder.id(), query_buffer.id()]; + app.dispatch_action(window_id, chain.clone(), "buffer:insert", "b".to_string()); + app.dispatch_action(window_id, chain.clone(), "buffer:insert", "n".to_string()); + app.dispatch_action(window_id, chain.clone(), "buffer:insert", "a".to_string()); + finder + .condition(&app, |finder, _| finder.matches.len() == 2) + .await; - // Can switch between different matches with the same relative path. - finder.update(&mut app, |f, ctx| { - assert_eq!(f.selected_index(), 0); - f.select_next(&(), ctx); - assert_eq!(f.selected_index(), 1); - f.select_prev(&(), ctx); - assert_eq!(f.selected_index(), 0); - }); + let active_pane = app.read(|ctx| workspace.read(ctx).active_pane().clone()); + app.dispatch_action( + window_id, + vec![workspace.id(), finder.id()], + "menu:select_next", + (), + ); + app.dispatch_action( + window_id, + vec![workspace.id(), finder.id()], + "file_finder:confirm", + (), + ); + active_pane + .condition(&app, |pane, _| pane.active_item().is_some()) + .await; + app.read(|ctx| { + let active_item = active_pane.read(ctx).active_item().unwrap(); + assert_eq!(active_item.title(ctx), "bandana"); + }); + } + + #[gpui::test] + async fn test_matching_cancellation(mut app: gpui::TestAppContext) { + let tmp_dir = temp_tree(json!({ + "hello": "", + "goodbye": "", + "halogen-light": "", + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + })); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings.clone(), ctx); + workspace.add_worktree(tmp_dir.path(), ctx); + workspace + }); + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; + let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx)); + + let query = "hi".to_string(); + finder.update(&mut app, |f, ctx| f.spawn_search(query.clone(), ctx)); + finder.condition(&app, |f, _| f.matches.len() == 5).await; + + finder.update(&mut app, |finder, ctx| { + let matches = finder.matches.clone(); + + // Simulate a search being cancelled after the time limit, + // returning only a subset of the matches that would have been found. + finder.spawn_search(query.clone(), ctx); + finder.update_matches( + ( + finder.latest_search_id, + true, // did-cancel + query.clone(), + vec![matches[1].clone(), matches[3].clone()], + ), + ctx, + ); + + // Simulate another cancellation. + finder.spawn_search(query.clone(), ctx); + finder.update_matches( + ( + finder.latest_search_id, + true, // did-cancel + query.clone(), + vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], + ), + ctx, + ); + + assert_eq!(finder.matches, matches[0..4]) + }); + } + + #[gpui::test] + async fn test_single_file_worktrees(mut app: gpui::TestAppContext) { + let temp_dir = TempDir::new("test-single-file-worktrees").unwrap(); + let dir_path = temp_dir.path().join("the-parent-dir"); + let file_path = dir_path.join("the-file"); + fs::create_dir(&dir_path).unwrap(); + fs::write(&file_path, "").unwrap(); + + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings.clone(), ctx); + workspace.add_worktree(&file_path, ctx); + workspace + }); + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; + let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx)); + + // Even though there is only one worktree, that worktree's filename + // is included in the matching, because the worktree is a single file. + finder.update(&mut app, |f, ctx| f.spawn_search("thf".into(), ctx)); + finder.condition(&app, |f, _| f.matches.len() == 1).await; + + app.read(|ctx| { + let finder = finder.read(ctx); + let (file_name, file_name_positions, full_path, full_path_positions) = + finder.labels_for_match(&finder.matches[0], ctx).unwrap(); + + assert_eq!(file_name, "the-file"); + assert_eq!(file_name_positions, &[0, 1, 4]); + assert_eq!(full_path, "the-file"); + assert_eq!(full_path_positions, &[0, 1, 4]); + }); + + // Since the worktree root is a file, searching for its name followed by a slash does + // not match anything. + finder.update(&mut app, |f, ctx| f.spawn_search("thf/".into(), ctx)); + finder.condition(&app, |f, _| f.matches.len() == 0).await; + } + + #[gpui::test] + async fn test_multiple_matches_with_same_relative_path(mut app: gpui::TestAppContext) { + let tmp_dir = temp_tree(json!({ + "dir1": { "a.txt": "" }, + "dir2": { "a.txt": "" } + })); + let settings = settings::channel(&app.font_cache()).unwrap().1; + + let (_, workspace) = app.add_window(|ctx| Workspace::new(0, settings.clone(), ctx)); + + workspace + .update(&mut app, |workspace, ctx| { + workspace.open_paths( + &[tmp_dir.path().join("dir1"), tmp_dir.path().join("dir2")], + ctx, + ) + }) + .await; + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; + + let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx)); + + // Run a search that matches two files with the same relative path. + finder.update(&mut app, |f, ctx| f.spawn_search("a.t".into(), ctx)); + finder.condition(&app, |f, _| f.matches.len() == 2).await; + + // Can switch between different matches with the same relative path. + finder.update(&mut app, |f, ctx| { + assert_eq!(f.selected_index(), 0); + f.select_next(&(), ctx); + assert_eq!(f.selected_index(), 1); + f.select_prev(&(), ctx); + assert_eq!(f.selected_index(), 0); }); } } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 1b25473173..90f22eaba9 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -729,7 +729,6 @@ impl WorkspaceHandle for ViewHandle { mod tests { use super::*; use crate::{editor::BufferView, settings, test::temp_tree}; - use gpui::App; use serde_json::json; use std::collections::HashSet; use tempdir::TempDir; @@ -793,332 +792,324 @@ mod tests { assert_eq!(app.window_ids().count(), 2); } - #[test] - fn test_open_entry() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - "a": { - "file1": "contents 1", - "file2": "contents 2", - "file3": "contents 3", - }, - })); + #[gpui::test] + async fn test_open_entry(mut app: gpui::TestAppContext) { + let dir = temp_tree(json!({ + "a": { + "file1": "contents 1", + "file2": "contents 2", + "file3": "contents 3", + }, + })); - let settings = settings::channel(&app.font_cache()).unwrap().1; + let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, workspace) = app.add_window(|ctx| { - let mut workspace = Workspace::new(0, settings, ctx); - workspace.add_worktree(dir.path(), ctx); - workspace - }); - - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; - let entries = app.read(|ctx| workspace.file_entries(ctx)); - let file1 = entries[0].clone(); - let file2 = entries[1].clone(); - let file3 = entries[2].clone(); - - // Open the first entry + let (_, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(dir.path(), ctx); workspace - .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)) - .unwrap() - .await; - app.read(|ctx| { - let pane = workspace.read(ctx).active_pane().read(ctx); - assert_eq!( - pane.active_item().unwrap().entry_id(ctx), - Some(file1.clone()) - ); - assert_eq!(pane.items().len(), 1); - }); - - // Open the second entry - workspace - .update(&mut app, |w, ctx| w.open_entry(file2.clone(), ctx)) - .unwrap() - .await; - app.read(|ctx| { - let pane = workspace.read(ctx).active_pane().read(ctx); - assert_eq!( - pane.active_item().unwrap().entry_id(ctx), - Some(file2.clone()) - ); - assert_eq!(pane.items().len(), 2); - }); - - // Open the first entry again. The existing pane item is activated. - workspace.update(&mut app, |w, ctx| { - assert!(w.open_entry(file1.clone(), ctx).is_none()) - }); - app.read(|ctx| { - let pane = workspace.read(ctx).active_pane().read(ctx); - assert_eq!( - pane.active_item().unwrap().entry_id(ctx), - Some(file1.clone()) - ); - assert_eq!(pane.items().len(), 2); - }); - - // Split the pane with the first entry, then open the second entry again. - workspace.update(&mut app, |w, ctx| { - w.split_pane(w.active_pane().clone(), SplitDirection::Right, ctx); - assert!(w.open_entry(file2.clone(), ctx).is_none()); - assert_eq!( - w.active_pane() - .read(ctx) - .active_item() - .unwrap() - .entry_id(ctx.as_ref()), - Some(file2.clone()) - ); - }); - - // Open the third entry twice concurrently. Two pane items - // are added. - let (t1, t2) = workspace.update(&mut app, |w, ctx| { - ( - w.open_entry(file3.clone(), ctx).unwrap(), - w.open_entry(file3.clone(), ctx).unwrap(), - ) - }); - t1.await; - t2.await; - app.read(|ctx| { - let pane = workspace.read(ctx).active_pane().read(ctx); - assert_eq!( - pane.active_item().unwrap().entry_id(ctx), - Some(file3.clone()) - ); - let pane_entries = pane - .items() - .iter() - .map(|i| i.entry_id(ctx).unwrap()) - .collect::>(); - assert_eq!(pane_entries, &[file1, file2, file3.clone(), file3]); - }); }); - } - #[test] - fn test_open_paths() { - App::test_async((), |mut app| async move { - let dir1 = temp_tree(json!({ - "a.txt": "", - })); - let dir2 = temp_tree(json!({ - "b.txt": "", - })); - - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, workspace) = app.add_window(|ctx| { - let mut workspace = Workspace::new(0, settings, ctx); - workspace.add_worktree(dir1.path(), ctx); - workspace - }); - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; - - // Open a file within an existing worktree. - app.update(|ctx| { - workspace.update(ctx, |view, ctx| { - view.open_paths(&[dir1.path().join("a.txt")], ctx) - }) - }) + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) .await; - app.read(|ctx| { - assert_eq!( - workspace - .read(ctx) - .active_pane() - .read(ctx) - .active_item() - .unwrap() - .title(ctx), - "a.txt" - ); - }); + let entries = app.read(|ctx| workspace.file_entries(ctx)); + let file1 = entries[0].clone(); + let file2 = entries[1].clone(); + let file3 = entries[2].clone(); - // Open a file outside of any existing worktree. - app.update(|ctx| { - workspace.update(ctx, |view, ctx| { - view.open_paths(&[dir2.path().join("b.txt")], ctx) - }) - }) + // Open the first entry + workspace + .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)) + .unwrap() .await; - app.read(|ctx| { - let worktree_roots = workspace + app.read(|ctx| { + let pane = workspace.read(ctx).active_pane().read(ctx); + assert_eq!( + pane.active_item().unwrap().entry_id(ctx), + Some(file1.clone()) + ); + assert_eq!(pane.items().len(), 1); + }); + + // Open the second entry + workspace + .update(&mut app, |w, ctx| w.open_entry(file2.clone(), ctx)) + .unwrap() + .await; + app.read(|ctx| { + let pane = workspace.read(ctx).active_pane().read(ctx); + assert_eq!( + pane.active_item().unwrap().entry_id(ctx), + Some(file2.clone()) + ); + assert_eq!(pane.items().len(), 2); + }); + + // Open the first entry again. The existing pane item is activated. + workspace.update(&mut app, |w, ctx| { + assert!(w.open_entry(file1.clone(), ctx).is_none()) + }); + app.read(|ctx| { + let pane = workspace.read(ctx).active_pane().read(ctx); + assert_eq!( + pane.active_item().unwrap().entry_id(ctx), + Some(file1.clone()) + ); + assert_eq!(pane.items().len(), 2); + }); + + // Split the pane with the first entry, then open the second entry again. + workspace.update(&mut app, |w, ctx| { + w.split_pane(w.active_pane().clone(), SplitDirection::Right, ctx); + assert!(w.open_entry(file2.clone(), ctx).is_none()); + assert_eq!( + w.active_pane() .read(ctx) - .worktrees() - .iter() - .map(|w| w.read(ctx).abs_path()) - .collect::>(); - assert_eq!( - worktree_roots, - vec![dir1.path(), &dir2.path().join("b.txt")] - .into_iter() - .collect(), - ); - assert_eq!( - workspace - .read(ctx) - .active_pane() - .read(ctx) - .active_item() - .unwrap() - .title(ctx), - "b.txt" - ); - }); + .active_item() + .unwrap() + .entry_id(ctx.as_ref()), + Some(file2.clone()) + ); + }); + + // Open the third entry twice concurrently. Two pane items + // are added. + let (t1, t2) = workspace.update(&mut app, |w, ctx| { + ( + w.open_entry(file3.clone(), ctx).unwrap(), + w.open_entry(file3.clone(), ctx).unwrap(), + ) + }); + t1.await; + t2.await; + app.read(|ctx| { + let pane = workspace.read(ctx).active_pane().read(ctx); + assert_eq!( + pane.active_item().unwrap().entry_id(ctx), + Some(file3.clone()) + ); + let pane_entries = pane + .items() + .iter() + .map(|i| i.entry_id(ctx).unwrap()) + .collect::>(); + assert_eq!(pane_entries, &[file1, file2, file3.clone(), file3]); }); } - #[test] - fn test_open_and_save_new_file() { - App::test_async((), |mut app| async move { - let dir = TempDir::new("test-new-file").unwrap(); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, workspace) = app.add_window(|ctx| { - let mut workspace = Workspace::new(0, settings, ctx); - workspace.add_worktree(dir.path(), ctx); - workspace - }); - let tree = app.read(|ctx| { + #[gpui::test] + async fn test_open_paths(mut app: gpui::TestAppContext) { + let dir1 = temp_tree(json!({ + "a.txt": "", + })); + let dir2 = temp_tree(json!({ + "b.txt": "", + })); + + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(dir1.path(), ctx); + workspace + }); + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; + + // Open a file within an existing worktree. + app.update(|ctx| { + workspace.update(ctx, |view, ctx| { + view.open_paths(&[dir1.path().join("a.txt")], ctx) + }) + }) + .await; + app.read(|ctx| { + assert_eq!( workspace .read(ctx) - .worktrees() - .iter() - .next() + .active_pane() + .read(ctx) + .active_item() .unwrap() - .clone() - }); - tree.flush_fs_events(&app).await; + .title(ctx), + "a.txt" + ); + }); - // Create a new untitled buffer - let editor = workspace.update(&mut app, |workspace, ctx| { - workspace.open_new_file(&(), ctx); - workspace - .active_item(ctx) - .unwrap() - .to_any() - .downcast::() - .unwrap() - }); - editor.update(&mut app, |editor, ctx| { - assert!(!editor.is_dirty(ctx.as_ref())); - assert_eq!(editor.title(ctx.as_ref()), "untitled"); - editor.insert(&"hi".to_string(), ctx); - assert!(editor.is_dirty(ctx.as_ref())); - }); - - // Save the buffer. This prompts for a filename. - workspace.update(&mut app, |workspace, ctx| { - workspace.save_active_item(&(), ctx) - }); - app.simulate_new_path_selection(|parent_dir| { - assert_eq!(parent_dir, dir.path()); - Some(parent_dir.join("the-new-name")) - }); - app.read(|ctx| { - assert!(editor.is_dirty(ctx)); - assert_eq!(editor.title(ctx), "untitled"); - }); - - // When the save completes, the buffer's title is updated. - tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) - .await; - app.read(|ctx| { - assert!(!editor.is_dirty(ctx)); - assert_eq!(editor.title(ctx), "the-new-name"); - }); - - // Edit the file and save it again. This time, there is no filename prompt. - editor.update(&mut app, |editor, ctx| { - editor.insert(&" there".to_string(), ctx); - assert_eq!(editor.is_dirty(ctx.as_ref()), true); - }); - workspace.update(&mut app, |workspace, ctx| { - workspace.save_active_item(&(), ctx) - }); - assert!(!app.did_prompt_for_new_path()); - editor - .condition(&app, |editor, ctx| !editor.is_dirty(ctx)) - .await; - app.read(|ctx| assert_eq!(editor.title(ctx), "the-new-name")); - - // Open the same newly-created file in another pane item. The new editor should reuse - // the same buffer. - workspace.update(&mut app, |workspace, ctx| { - workspace.open_new_file(&(), ctx); - workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, ctx); - assert!(workspace - .open_entry((tree.id(), Path::new("the-new-name").into()), ctx) - .is_none()); - }); - let editor2 = workspace.update(&mut app, |workspace, ctx| { - workspace - .active_item(ctx) - .unwrap() - .to_any() - .downcast::() - .unwrap() - }); - app.read(|ctx| { - assert_eq!(editor2.read(ctx).buffer(), editor.read(ctx).buffer()); + // Open a file outside of any existing worktree. + app.update(|ctx| { + workspace.update(ctx, |view, ctx| { + view.open_paths(&[dir2.path().join("b.txt")], ctx) }) + }) + .await; + app.read(|ctx| { + let worktree_roots = workspace + .read(ctx) + .worktrees() + .iter() + .map(|w| w.read(ctx).abs_path()) + .collect::>(); + assert_eq!( + worktree_roots, + vec![dir1.path(), &dir2.path().join("b.txt")] + .into_iter() + .collect(), + ); + assert_eq!( + workspace + .read(ctx) + .active_pane() + .read(ctx) + .active_item() + .unwrap() + .title(ctx), + "b.txt" + ); }); } - #[test] - fn test_pane_actions() { - App::test_async((), |mut app| async move { - app.update(|ctx| pane::init(ctx)); - - let dir = temp_tree(json!({ - "a": { - "file1": "contents 1", - "file2": "contents 2", - "file3": "contents 3", - }, - })); - - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (window_id, workspace) = app.add_window(|ctx| { - let mut workspace = Workspace::new(0, settings, ctx); - workspace.add_worktree(dir.path(), ctx); - workspace - }); - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; - let entries = app.read(|ctx| workspace.file_entries(ctx)); - let file1 = entries[0].clone(); - - let pane_1 = app.read(|ctx| workspace.read(ctx).active_pane().clone()); - + #[gpui::test] + async fn test_open_and_save_new_file(mut app: gpui::TestAppContext) { + let dir = TempDir::new("test-new-file").unwrap(); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(dir.path(), ctx); workspace - .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)) + }); + let tree = app.read(|ctx| { + workspace + .read(ctx) + .worktrees() + .iter() + .next() .unwrap() - .await; - app.read(|ctx| { - assert_eq!( - pane_1.read(ctx).active_item().unwrap().entry_id(ctx), - Some(file1.clone()) - ); - }); + .clone() + }); + tree.flush_fs_events(&app).await; - app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ()); - app.update(|ctx| { - let pane_2 = workspace.read(ctx).active_pane().clone(); - assert_ne!(pane_1, pane_2); + // Create a new untitled buffer + let editor = workspace.update(&mut app, |workspace, ctx| { + workspace.open_new_file(&(), ctx); + workspace + .active_item(ctx) + .unwrap() + .to_any() + .downcast::() + .unwrap() + }); + editor.update(&mut app, |editor, ctx| { + assert!(!editor.is_dirty(ctx.as_ref())); + assert_eq!(editor.title(ctx.as_ref()), "untitled"); + editor.insert(&"hi".to_string(), ctx); + assert!(editor.is_dirty(ctx.as_ref())); + }); - let pane2_item = pane_2.read(ctx).active_item().unwrap(); - assert_eq!(pane2_item.entry_id(ctx.as_ref()), Some(file1.clone())); + // Save the buffer. This prompts for a filename. + workspace.update(&mut app, |workspace, ctx| { + workspace.save_active_item(&(), ctx) + }); + app.simulate_new_path_selection(|parent_dir| { + assert_eq!(parent_dir, dir.path()); + Some(parent_dir.join("the-new-name")) + }); + app.read(|ctx| { + assert!(editor.is_dirty(ctx)); + assert_eq!(editor.title(ctx), "untitled"); + }); - ctx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ()); - let workspace_view = workspace.read(ctx); - assert_eq!(workspace_view.panes.len(), 1); - assert_eq!(workspace_view.active_pane(), &pane_1); - }); + // When the save completes, the buffer's title is updated. + tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) + .await; + app.read(|ctx| { + assert!(!editor.is_dirty(ctx)); + assert_eq!(editor.title(ctx), "the-new-name"); + }); + + // Edit the file and save it again. This time, there is no filename prompt. + editor.update(&mut app, |editor, ctx| { + editor.insert(&" there".to_string(), ctx); + assert_eq!(editor.is_dirty(ctx.as_ref()), true); + }); + workspace.update(&mut app, |workspace, ctx| { + workspace.save_active_item(&(), ctx) + }); + assert!(!app.did_prompt_for_new_path()); + editor + .condition(&app, |editor, ctx| !editor.is_dirty(ctx)) + .await; + app.read(|ctx| assert_eq!(editor.title(ctx), "the-new-name")); + + // Open the same newly-created file in another pane item. The new editor should reuse + // the same buffer. + workspace.update(&mut app, |workspace, ctx| { + workspace.open_new_file(&(), ctx); + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, ctx); + assert!(workspace + .open_entry((tree.id(), Path::new("the-new-name").into()), ctx) + .is_none()); + }); + let editor2 = workspace.update(&mut app, |workspace, ctx| { + workspace + .active_item(ctx) + .unwrap() + .to_any() + .downcast::() + .unwrap() + }); + app.read(|ctx| { + assert_eq!(editor2.read(ctx).buffer(), editor.read(ctx).buffer()); + }) + } + + #[gpui::test] + async fn test_pane_actions(mut app: gpui::TestAppContext) { + app.update(|ctx| pane::init(ctx)); + + let dir = temp_tree(json!({ + "a": { + "file1": "contents 1", + "file2": "contents 2", + "file3": "contents 3", + }, + })); + + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (window_id, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(dir.path(), ctx); + workspace + }); + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; + let entries = app.read(|ctx| workspace.file_entries(ctx)); + let file1 = entries[0].clone(); + + let pane_1 = app.read(|ctx| workspace.read(ctx).active_pane().clone()); + + workspace + .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)) + .unwrap() + .await; + app.read(|ctx| { + assert_eq!( + pane_1.read(ctx).active_item().unwrap().entry_id(ctx), + Some(file1.clone()) + ); + }); + + app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ()); + app.update(|ctx| { + let pane_2 = workspace.read(ctx).active_pane().clone(); + assert_ne!(pane_1, pane_2); + + let pane2_item = pane_2.read(ctx).active_item().unwrap(); + assert_eq!(pane2_item.entry_id(ctx.as_ref()), Some(file1.clone())); + + ctx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ()); + let workspace_view = workspace.read(ctx); + assert_eq!(workspace_view.panes.len(), 1); + assert_eq!(workspace_view.active_pane(), &pane_1); }); } } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index be895533f1..30383f5a0a 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1353,7 +1353,6 @@ mod tests { use crate::editor::Buffer; use crate::test::*; use anyhow::Result; - use gpui::App; use rand::prelude::*; use serde_json::json; use std::env; @@ -1361,248 +1360,237 @@ mod tests { use std::os::unix; use std::time::{SystemTime, UNIX_EPOCH}; - #[test] - fn test_populate_and_search() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - "root": { - "apple": "", - "banana": { - "carrot": { - "date": "", - "endive": "", - } - }, - "fennel": { - "grape": "", + #[gpui::test] + async fn test_populate_and_search(mut app: gpui::TestAppContext) { + let dir = temp_tree(json!({ + "root": { + "apple": "", + "banana": { + "carrot": { + "date": "", + "endive": "", } + }, + "fennel": { + "grape": "", } - })); + } + })); - let root_link_path = dir.path().join("root_link"); - unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap(); - unix::fs::symlink( - &dir.path().join("root/fennel"), - &dir.path().join("root/finnochio"), + let root_link_path = dir.path().join("root_link"); + unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap(); + unix::fs::symlink( + &dir.path().join("root/fennel"), + &dir.path().join("root/finnochio"), + ) + .unwrap(); + + let tree = app.add_model(|ctx| Worktree::new(root_link_path, ctx)); + + app.read(|ctx| tree.read(ctx).scan_complete()).await; + app.read(|ctx| { + let tree = tree.read(ctx); + assert_eq!(tree.file_count(), 5); + + assert_eq!( + tree.inode_for_path("fennel/grape"), + tree.inode_for_path("finnochio/grape") + ); + + let results = match_paths( + Some(tree.snapshot()).iter(), + "bna", + false, + false, + false, + 10, + Default::default(), + ctx.thread_pool().clone(), ) + .into_iter() + .map(|result| result.path) + .collect::>>(); + assert_eq!( + results, + vec![ + PathBuf::from("banana/carrot/date").into(), + PathBuf::from("banana/carrot/endive").into(), + ] + ); + }) + } + + #[gpui::test] + async fn test_save_file(mut app: gpui::TestAppContext) { + let dir = temp_tree(json!({ + "file1": "the old contents", + })); + + let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); + app.read(|ctx| tree.read(ctx).scan_complete()).await; + app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1)); + + let buffer = + app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx)); + + let path = tree.update(&mut app, |tree, ctx| { + let path = tree.files(0).next().unwrap().path().clone(); + assert_eq!(path.file_name().unwrap(), "file1"); + smol::block_on(tree.save(&path, buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap(); + path + }); + + let history = app + .read(|ctx| tree.read(ctx).load_history(&path, ctx)) + .await .unwrap(); - - let tree = app.add_model(|ctx| Worktree::new(root_link_path, ctx)); - - app.read(|ctx| tree.read(ctx).scan_complete()).await; - app.read(|ctx| { - let tree = tree.read(ctx); - assert_eq!(tree.file_count(), 5); - - assert_eq!( - tree.inode_for_path("fennel/grape"), - tree.inode_for_path("finnochio/grape") - ); - - let results = match_paths( - Some(tree.snapshot()).iter(), - "bna", - false, - false, - false, - 10, - Default::default(), - ctx.thread_pool().clone(), - ) - .into_iter() - .map(|result| result.path) - .collect::>>(); - assert_eq!( - results, - vec![ - PathBuf::from("banana/carrot/date").into(), - PathBuf::from("banana/carrot/endive").into(), - ] - ); - }) + app.read(|ctx| { + assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text()); }); } - #[test] - fn test_save_file() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - "file1": "the old contents", - })); + #[gpui::test] + async fn test_save_in_single_file_worktree(mut app: gpui::TestAppContext) { + let dir = temp_tree(json!({ + "file1": "the old contents", + })); - let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); - app.read(|ctx| tree.read(ctx).scan_complete()).await; - app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1)); + let tree = app.add_model(|ctx| Worktree::new(dir.path().join("file1"), ctx)); + app.read(|ctx| tree.read(ctx).scan_complete()).await; + app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1)); - let buffer = - app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx)); + let buffer = + app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx)); - let path = tree.update(&mut app, |tree, ctx| { - let path = tree.files(0).next().unwrap().path().clone(); - assert_eq!(path.file_name().unwrap(), "file1"); - smol::block_on(tree.save(&path, buffer.read(ctx).snapshot(), ctx.as_ref())) - .unwrap(); - path - }); - - let history = app - .read(|ctx| tree.read(ctx).load_history(&path, ctx)) - .await - .unwrap(); - app.read(|ctx| { - assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text()); - }); + let file = app.read(|ctx| tree.file("", ctx)); + app.update(|ctx| { + assert_eq!(file.path().file_name(), None); + smol::block_on(file.save(buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap(); }); + + let history = app.read(|ctx| file.load_history(ctx)).await.unwrap(); + app.read(|ctx| assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text())); } - #[test] - fn test_save_in_single_file_worktree() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - "file1": "the old contents", - })); - - let tree = app.add_model(|ctx| Worktree::new(dir.path().join("file1"), ctx)); - app.read(|ctx| tree.read(ctx).scan_complete()).await; - app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1)); - - let buffer = - app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx)); - - let file = app.read(|ctx| tree.file("", ctx)); - app.update(|ctx| { - assert_eq!(file.path().file_name(), None); - smol::block_on(file.save(buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap(); - }); - - let history = app.read(|ctx| file.load_history(ctx)).await.unwrap(); - app.read(|ctx| assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text())); - }); - } - - #[test] - fn test_rescan_simple() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - "a": { - "file1": "", - "file2": "", - "file3": "", - }, - "b": { - "c": { - "file4": "", - "file5": "", - } + #[gpui::test] + async fn test_rescan_simple(mut app: gpui::TestAppContext) { + let dir = temp_tree(json!({ + "a": { + "file1": "", + "file2": "", + "file3": "", + }, + "b": { + "c": { + "file4": "", + "file5": "", } - })); + } + })); - let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); - let (file2, file3, file4, file5, non_existent_file) = app.read(|ctx| { - ( - tree.file("a/file2", ctx), - tree.file("a/file3", ctx), - tree.file("b/c/file4", ctx), - tree.file("b/c/file5", ctx), - tree.file("a/filex", ctx), - ) - }); + let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); + let (file2, file3, file4, file5, non_existent_file) = app.read(|ctx| { + ( + tree.file("a/file2", ctx), + tree.file("a/file3", ctx), + tree.file("b/c/file4", ctx), + tree.file("b/c/file5", ctx), + tree.file("a/filex", ctx), + ) + }); - // The worktree hasn't scanned the directories containing these paths, - // so it can't determine that the paths are deleted. + // The worktree hasn't scanned the directories containing these paths, + // so it can't determine that the paths are deleted. + assert!(!file2.is_deleted()); + assert!(!file3.is_deleted()); + assert!(!file4.is_deleted()); + assert!(!file5.is_deleted()); + assert!(!non_existent_file.is_deleted()); + + // After scanning, the worktree knows which files exist and which don't. + app.read(|ctx| tree.read(ctx).scan_complete()).await; + assert!(!file2.is_deleted()); + assert!(!file3.is_deleted()); + assert!(!file4.is_deleted()); + assert!(!file5.is_deleted()); + assert!(non_existent_file.is_deleted()); + + tree.flush_fs_events(&app).await; + std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap(); + std::fs::remove_file(dir.path().join("b/c/file5")).unwrap(); + std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap(); + std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap(); + tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) + .await; + + app.read(|ctx| { + assert_eq!( + tree.read(ctx) + .paths() + .map(|p| p.to_str().unwrap()) + .collect::>(), + vec![ + "a", + "a/file1", + "a/file2.new", + "b", + "d", + "d/file3", + "d/file4" + ] + ); + + assert_eq!(file2.path().to_str().unwrap(), "a/file2.new"); + assert_eq!(file4.path().as_ref(), Path::new("d/file4")); + assert_eq!(file5.path().as_ref(), Path::new("d/file5")); assert!(!file2.is_deleted()); - assert!(!file3.is_deleted()); assert!(!file4.is_deleted()); - assert!(!file5.is_deleted()); - assert!(!non_existent_file.is_deleted()); + assert!(file5.is_deleted()); - // After scanning, the worktree knows which files exist and which don't. - app.read(|ctx| tree.read(ctx).scan_complete()).await; - assert!(!file2.is_deleted()); - assert!(!file3.is_deleted()); - assert!(!file4.is_deleted()); - assert!(!file5.is_deleted()); - assert!(non_existent_file.is_deleted()); - - tree.flush_fs_events(&app).await; - std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap(); - std::fs::remove_file(dir.path().join("b/c/file5")).unwrap(); - std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap(); - std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap(); - tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) - .await; - - app.read(|ctx| { - assert_eq!( - tree.read(ctx) - .paths() - .map(|p| p.to_str().unwrap()) - .collect::>(), - vec![ - "a", - "a/file1", - "a/file2.new", - "b", - "d", - "d/file3", - "d/file4" - ] - ); - - assert_eq!(file2.path().to_str().unwrap(), "a/file2.new"); - assert_eq!(file4.path().as_ref(), Path::new("d/file4")); - assert_eq!(file5.path().as_ref(), Path::new("d/file5")); - assert!(!file2.is_deleted()); - assert!(!file4.is_deleted()); - assert!(file5.is_deleted()); - - // Right now, this rename isn't detected because the target path - // no longer exists on the file system by the time we process the - // rename event. - assert_eq!(file3.path().as_ref(), Path::new("a/file3")); - assert!(file3.is_deleted()); - }); + // Right now, this rename isn't detected because the target path + // no longer exists on the file system by the time we process the + // rename event. + assert_eq!(file3.path().as_ref(), Path::new("a/file3")); + assert!(file3.is_deleted()); }); } - #[test] - fn test_rescan_with_gitignore() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - ".git": {}, - ".gitignore": "ignored-dir\n", - "tracked-dir": { - "tracked-file1": "tracked contents", - }, - "ignored-dir": { - "ignored-file1": "ignored contents", - } - })); + #[gpui::test] + async fn test_rescan_with_gitignore(mut app: gpui::TestAppContext) { + let dir = temp_tree(json!({ + ".git": {}, + ".gitignore": "ignored-dir\n", + "tracked-dir": { + "tracked-file1": "tracked contents", + }, + "ignored-dir": { + "ignored-file1": "ignored contents", + } + })); - let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); - app.read(|ctx| tree.read(ctx).scan_complete()).await; - tree.flush_fs_events(&app).await; - app.read(|ctx| { - let tree = tree.read(ctx); - let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap(); - let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap(); - assert_eq!(tracked.is_ignored(), false); - assert_eq!(ignored.is_ignored(), true); - }); + let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); + app.read(|ctx| tree.read(ctx).scan_complete()).await; + tree.flush_fs_events(&app).await; + app.read(|ctx| { + let tree = tree.read(ctx); + let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap(); + let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap(); + assert_eq!(tracked.is_ignored(), false); + assert_eq!(ignored.is_ignored(), true); + }); - fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap(); - fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap(); - tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) - .await; - app.read(|ctx| { - let tree = tree.read(ctx); - let dot_git = tree.entry_for_path(".git").unwrap(); - let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap(); - let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap(); - assert_eq!(tracked.is_ignored(), false); - assert_eq!(ignored.is_ignored(), true); - assert_eq!(dot_git.is_ignored(), true); - }); + fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap(); + fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap(); + tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) + .await; + app.read(|ctx| { + let tree = tree.read(ctx); + let dot_git = tree.entry_for_path(".git").unwrap(); + let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap(); + let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap(); + assert_eq!(tracked.is_ignored(), false); + assert_eq!(ignored.is_ignored(), true); + assert_eq!(dot_git.is_ignored(), true); }); } From 2326ac3dbfdc6e14442cc6396c1de999ef1b4706 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 12 May 2021 18:21:32 +0200 Subject: [PATCH 4/7] Use `gpui::test` for tests in gpui Co-Authored-By: Max Brunsfeld --- gpui/src/app.rs | 840 +++++++++++++++++++---------------------- gpui_macros/src/lib.rs | 34 +- 2 files changed, 423 insertions(+), 451 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 178b8f9b69..446220773a 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -2797,8 +2797,8 @@ mod tests { use crate::elements::*; use smol::future::poll_once; - #[test] - fn test_model_handles() { + #[crate::test(self)] + fn test_model_handles(app: &mut MutableAppContext) { struct Model { other: Option>, events: Vec, @@ -2826,40 +2826,38 @@ mod tests { } } - App::test((), |app| { - let handle_1 = app.add_model(|ctx| Model::new(None, ctx)); - let handle_2 = app.add_model(|ctx| Model::new(Some(handle_1.clone()), ctx)); - assert_eq!(app.ctx.models.len(), 2); + let handle_1 = app.add_model(|ctx| Model::new(None, ctx)); + let handle_2 = app.add_model(|ctx| Model::new(Some(handle_1.clone()), ctx)); + assert_eq!(app.ctx.models.len(), 2); - handle_1.update(app, |model, ctx| { - model.events.push("updated".into()); - ctx.emit(1); - ctx.notify(); - ctx.emit(2); - }); - assert_eq!(handle_1.read(app).events, vec!["updated".to_string()]); - assert_eq!( - handle_2.read(app).events, - vec![ - "observed event 1".to_string(), - "notified".to_string(), - "observed event 2".to_string(), - ] - ); - - handle_2.update(app, |model, _| { - drop(handle_1); - model.other.take(); - }); - - assert_eq!(app.ctx.models.len(), 1); - assert!(app.subscriptions.is_empty()); - assert!(app.model_observations.is_empty()); + handle_1.update(app, |model, ctx| { + model.events.push("updated".into()); + ctx.emit(1); + ctx.notify(); + ctx.emit(2); }); + assert_eq!(handle_1.read(app).events, vec!["updated".to_string()]); + assert_eq!( + handle_2.read(app).events, + vec![ + "observed event 1".to_string(), + "notified".to_string(), + "observed event 2".to_string(), + ] + ); + + handle_2.update(app, |model, _| { + drop(handle_1); + model.other.take(); + }); + + assert_eq!(app.ctx.models.len(), 1); + assert!(app.subscriptions.is_empty()); + assert!(app.model_observations.is_empty()); } - #[test] - fn test_subscribe_and_emit_from_model() { + #[crate::test(self)] + fn test_subscribe_and_emit_from_model(app: &mut MutableAppContext) { #[derive(Default)] struct Model { events: Vec, @@ -2869,31 +2867,29 @@ mod tests { type Event = usize; } - App::test((), |app| { - let handle_1 = app.add_model(|_| Model::default()); - let handle_2 = app.add_model(|_| Model::default()); - let handle_2b = handle_2.clone(); + let handle_1 = app.add_model(|_| Model::default()); + let handle_2 = app.add_model(|_| Model::default()); + let handle_2b = handle_2.clone(); - handle_1.update(app, |_, c| { - c.subscribe(&handle_2, move |model: &mut Model, event, c| { - model.events.push(*event); + handle_1.update(app, |_, c| { + c.subscribe(&handle_2, move |model: &mut Model, event, c| { + model.events.push(*event); - c.subscribe(&handle_2b, |model, event, _| { - model.events.push(*event * 2); - }); + c.subscribe(&handle_2b, |model, event, _| { + model.events.push(*event * 2); }); }); + }); - handle_2.update(app, |_, c| c.emit(7)); - assert_eq!(handle_1.read(app).events, vec![7]); + handle_2.update(app, |_, c| c.emit(7)); + assert_eq!(handle_1.read(app).events, vec![7]); - handle_2.update(app, |_, c| c.emit(5)); - assert_eq!(handle_1.read(app).events, vec![7, 10, 5]); - }) + handle_2.update(app, |_, c| c.emit(5)); + assert_eq!(handle_1.read(app).events, vec![7, 10, 5]); } - #[test] - fn test_observe_and_notify_from_model() { + #[crate::test(self)] + fn test_observe_and_notify_from_model(app: &mut MutableAppContext) { #[derive(Default)] struct Model { count: usize, @@ -2904,36 +2900,34 @@ mod tests { type Event = (); } - App::test((), |app| { - let handle_1 = app.add_model(|_| Model::default()); - let handle_2 = app.add_model(|_| Model::default()); - let handle_2b = handle_2.clone(); + let handle_1 = app.add_model(|_| Model::default()); + let handle_2 = app.add_model(|_| Model::default()); + let handle_2b = handle_2.clone(); - handle_1.update(app, |_, c| { - c.observe(&handle_2, move |model, observed, c| { - model.events.push(observed.read(c).count); - c.observe(&handle_2b, |model, observed, c| { - model.events.push(observed.read(c).count * 2); - }); + handle_1.update(app, |_, c| { + c.observe(&handle_2, move |model, observed, c| { + model.events.push(observed.read(c).count); + c.observe(&handle_2b, |model, observed, c| { + model.events.push(observed.read(c).count * 2); }); }); + }); - handle_2.update(app, |model, c| { - model.count = 7; - c.notify() - }); - assert_eq!(handle_1.read(app).events, vec![7]); + handle_2.update(app, |model, c| { + model.count = 7; + c.notify() + }); + assert_eq!(handle_1.read(app).events, vec![7]); - handle_2.update(app, |model, c| { - model.count = 5; - c.notify() - }); - assert_eq!(handle_1.read(app).events, vec![7, 10, 5]) - }) + handle_2.update(app, |model, c| { + model.count = 5; + c.notify() + }); + assert_eq!(handle_1.read(app).events, vec![7, 10, 5]) } - #[test] - fn test_spawn_from_model() { + #[crate::test(self)] + async fn test_spawn_from_model(mut app: TestAppContext) { #[derive(Default)] struct Model { count: usize, @@ -2943,30 +2937,28 @@ mod tests { type Event = (); } - App::test_async((), |mut app| async move { - let handle = app.add_model(|_| Model::default()); - handle - .update(&mut app, |_, c| { - c.spawn(async { 7 }, |model, output, _| { - model.count = output; - }) + let handle = app.add_model(|_| Model::default()); + handle + .update(&mut app, |_, c| { + c.spawn(async { 7 }, |model, output, _| { + model.count = output; }) - .await; - app.read(|ctx| assert_eq!(handle.read(ctx).count, 7)); + }) + .await; + app.read(|ctx| assert_eq!(handle.read(ctx).count, 7)); - handle - .update(&mut app, |_, c| { - c.spawn(async { 14 }, |model, output, _| { - model.count = output; - }) + handle + .update(&mut app, |_, c| { + c.spawn(async { 14 }, |model, output, _| { + model.count = output; }) - .await; - app.read(|ctx| assert_eq!(handle.read(ctx).count, 14)); - }); + }) + .await; + app.read(|ctx| assert_eq!(handle.read(ctx).count, 14)); } - #[test] - fn test_spawn_stream_local_from_model() { + #[crate::test(self)] + async fn test_spawn_stream_local_from_model(mut app: TestAppContext) { #[derive(Default)] struct Model { events: Vec>, @@ -2976,27 +2968,21 @@ mod tests { type Event = (); } - App::test_async((), |mut app| async move { - let handle = app.add_model(|_| Model::default()); - handle - .update(&mut app, |_, c| { - c.spawn_stream( - smol::stream::iter(vec![1, 2, 3]), - |model, output, _| { - model.events.push(Some(output)); - }, - |model, _| { - model.events.push(None); - }, - ) - }) - .await; - app.read(|ctx| assert_eq!(handle.read(ctx).events, [Some(1), Some(2), Some(3), None])); - }) + let handle = app.add_model(|_| Model::default()); + handle + .update(&mut app, |_, c| { + c.spawn_stream( + smol::stream::iter(vec![1, 2, 3]), + |model, output, _| model.events.push(Some(output)), + |model, _| model.events.push(None), + ) + }) + .await; + app.read(|ctx| assert_eq!(handle.read(ctx).events, [Some(1), Some(2), Some(3), None])); } - #[test] - fn test_view_handles() { + #[crate::test(self)] + fn test_view_handles(app: &mut MutableAppContext) { struct View { other: Option>, events: Vec, @@ -3030,39 +3016,37 @@ mod tests { } } - App::test((), |app| { - let (window_id, _) = app.add_window(|ctx| View::new(None, ctx)); - let handle_1 = app.add_view(window_id, |ctx| View::new(None, ctx)); - let handle_2 = app.add_view(window_id, |ctx| View::new(Some(handle_1.clone()), ctx)); - assert_eq!(app.ctx.views.len(), 3); + let (window_id, _) = app.add_window(|ctx| View::new(None, ctx)); + let handle_1 = app.add_view(window_id, |ctx| View::new(None, ctx)); + let handle_2 = app.add_view(window_id, |ctx| View::new(Some(handle_1.clone()), ctx)); + assert_eq!(app.ctx.views.len(), 3); - handle_1.update(app, |view, ctx| { - view.events.push("updated".into()); - ctx.emit(1); - ctx.emit(2); - }); - assert_eq!(handle_1.read(app).events, vec!["updated".to_string()]); - assert_eq!( - handle_2.read(app).events, - vec![ - "observed event 1".to_string(), - "observed event 2".to_string(), - ] - ); + handle_1.update(app, |view, ctx| { + view.events.push("updated".into()); + ctx.emit(1); + ctx.emit(2); + }); + assert_eq!(handle_1.read(app).events, vec!["updated".to_string()]); + assert_eq!( + handle_2.read(app).events, + vec![ + "observed event 1".to_string(), + "observed event 2".to_string(), + ] + ); - handle_2.update(app, |view, _| { - drop(handle_1); - view.other.take(); - }); + handle_2.update(app, |view, _| { + drop(handle_1); + view.other.take(); + }); - assert_eq!(app.ctx.views.len(), 2); - assert!(app.subscriptions.is_empty()); - assert!(app.model_observations.is_empty()); - }) + assert_eq!(app.ctx.views.len(), 2); + assert!(app.subscriptions.is_empty()); + assert!(app.model_observations.is_empty()); } - #[test] - fn test_subscribe_and_emit_from_view() { + #[crate::test(self)] + fn test_subscribe_and_emit_from_view(app: &mut MutableAppContext) { #[derive(Default)] struct View { events: Vec, @@ -3088,39 +3072,37 @@ mod tests { type Event = usize; } - App::test((), |app| { - let (window_id, handle_1) = app.add_window(|_| View::default()); - let handle_2 = app.add_view(window_id, |_| View::default()); - let handle_2b = handle_2.clone(); - let handle_3 = app.add_model(|_| Model); + let (window_id, handle_1) = app.add_window(|_| View::default()); + let handle_2 = app.add_view(window_id, |_| View::default()); + let handle_2b = handle_2.clone(); + let handle_3 = app.add_model(|_| Model); - handle_1.update(app, |_, c| { - c.subscribe_to_view(&handle_2, move |me, _, event, c| { - me.events.push(*event); + handle_1.update(app, |_, c| { + c.subscribe_to_view(&handle_2, move |me, _, event, c| { + me.events.push(*event); - c.subscribe_to_view(&handle_2b, |me, _, event, _| { - me.events.push(*event * 2); - }); + c.subscribe_to_view(&handle_2b, |me, _, event, _| { + me.events.push(*event * 2); }); - - c.subscribe_to_model(&handle_3, |me, _, event, _| { - me.events.push(*event); - }) }); - handle_2.update(app, |_, c| c.emit(7)); - assert_eq!(handle_1.read(app).events, vec![7]); + c.subscribe_to_model(&handle_3, |me, _, event, _| { + me.events.push(*event); + }) + }); - handle_2.update(app, |_, c| c.emit(5)); - assert_eq!(handle_1.read(app).events, vec![7, 10, 5]); + handle_2.update(app, |_, c| c.emit(7)); + assert_eq!(handle_1.read(app).events, vec![7]); - handle_3.update(app, |_, c| c.emit(9)); - assert_eq!(handle_1.read(app).events, vec![7, 10, 5, 9]); - }) + handle_2.update(app, |_, c| c.emit(5)); + assert_eq!(handle_1.read(app).events, vec![7, 10, 5]); + + handle_3.update(app, |_, c| c.emit(9)); + assert_eq!(handle_1.read(app).events, vec![7, 10, 5, 9]); } - #[test] - fn test_dropping_subscribers() { + #[crate::test(self)] + fn test_dropping_subscribers(app: &mut MutableAppContext) { struct View; impl Entity for View { @@ -3143,33 +3125,31 @@ mod tests { type Event = (); } - App::test((), |app| { - let (window_id, _) = app.add_window(|_| View); - let observing_view = app.add_view(window_id, |_| View); - let emitting_view = app.add_view(window_id, |_| View); - let observing_model = app.add_model(|_| Model); - let observed_model = app.add_model(|_| Model); + let (window_id, _) = app.add_window(|_| View); + let observing_view = app.add_view(window_id, |_| View); + let emitting_view = app.add_view(window_id, |_| View); + let observing_model = app.add_model(|_| Model); + let observed_model = app.add_model(|_| Model); - observing_view.update(app, |_, ctx| { - ctx.subscribe_to_view(&emitting_view, |_, _, _, _| {}); - ctx.subscribe_to_model(&observed_model, |_, _, _, _| {}); - }); - observing_model.update(app, |_, ctx| { - ctx.subscribe(&observed_model, |_, _, _| {}); - }); + observing_view.update(app, |_, ctx| { + ctx.subscribe_to_view(&emitting_view, |_, _, _, _| {}); + ctx.subscribe_to_model(&observed_model, |_, _, _, _| {}); + }); + observing_model.update(app, |_, ctx| { + ctx.subscribe(&observed_model, |_, _, _| {}); + }); - app.update(|| { - drop(observing_view); - drop(observing_model); - }); + app.update(|| { + drop(observing_view); + drop(observing_model); + }); - emitting_view.update(app, |_, ctx| ctx.emit(())); - observed_model.update(app, |_, ctx| ctx.emit(())); - }) + emitting_view.update(app, |_, ctx| ctx.emit(())); + observed_model.update(app, |_, ctx| ctx.emit(())); } - #[test] - fn test_observe_and_notify_from_view() { + #[crate::test(self)] + fn test_observe_and_notify_from_view(app: &mut MutableAppContext) { #[derive(Default)] struct View { events: Vec, @@ -3198,26 +3178,24 @@ mod tests { type Event = (); } - App::test((), |app| { - let (_, view) = app.add_window(|_| View::default()); - let model = app.add_model(|_| Model::default()); + let (_, view) = app.add_window(|_| View::default()); + let model = app.add_model(|_| Model::default()); - view.update(app, |_, c| { - c.observe_model(&model, |me, observed, c| { - me.events.push(observed.read(c).count) - }); + view.update(app, |_, c| { + c.observe_model(&model, |me, observed, c| { + me.events.push(observed.read(c).count) }); + }); - model.update(app, |model, c| { - model.count = 11; - c.notify(); - }); - assert_eq!(view.read(app).events, vec![11]); - }) + model.update(app, |model, c| { + model.count = 11; + c.notify(); + }); + assert_eq!(view.read(app).events, vec![11]); } - #[test] - fn test_dropping_observers() { + #[crate::test(self)] + fn test_dropping_observers(app: &mut MutableAppContext) { struct View; impl Entity for View { @@ -3240,30 +3218,28 @@ mod tests { type Event = (); } - App::test((), |app| { - let (window_id, _) = app.add_window(|_| View); - let observing_view = app.add_view(window_id, |_| View); - let observing_model = app.add_model(|_| Model); - let observed_model = app.add_model(|_| Model); + let (window_id, _) = app.add_window(|_| View); + let observing_view = app.add_view(window_id, |_| View); + let observing_model = app.add_model(|_| Model); + let observed_model = app.add_model(|_| Model); - observing_view.update(app, |_, ctx| { - ctx.observe_model(&observed_model, |_, _, _| {}); - }); - observing_model.update(app, |_, ctx| { - ctx.observe(&observed_model, |_, _, _| {}); - }); + observing_view.update(app, |_, ctx| { + ctx.observe_model(&observed_model, |_, _, _| {}); + }); + observing_model.update(app, |_, ctx| { + ctx.observe(&observed_model, |_, _, _| {}); + }); - app.update(|| { - drop(observing_view); - drop(observing_model); - }); + app.update(|| { + drop(observing_view); + drop(observing_model); + }); - observed_model.update(app, |_, ctx| ctx.notify()); - }) + observed_model.update(app, |_, ctx| ctx.notify()); } - #[test] - fn test_focus() { + #[crate::test(self)] + fn test_focus(app: &mut MutableAppContext) { struct View { name: String, events: Arc>>, @@ -3291,40 +3267,38 @@ mod tests { } } - App::test((), |app| { - let events: Arc>> = Default::default(); - let (window_id, view_1) = app.add_window(|_| View { - events: events.clone(), - name: "view 1".to_string(), - }); - let view_2 = app.add_view(window_id, |_| View { - events: events.clone(), - name: "view 2".to_string(), - }); + let events: Arc>> = Default::default(); + let (window_id, view_1) = app.add_window(|_| View { + events: events.clone(), + name: "view 1".to_string(), + }); + let view_2 = app.add_view(window_id, |_| View { + events: events.clone(), + name: "view 2".to_string(), + }); - view_1.update(app, |_, ctx| ctx.focus(&view_2)); - view_1.update(app, |_, ctx| ctx.focus(&view_1)); - view_1.update(app, |_, ctx| ctx.focus(&view_2)); - view_1.update(app, |_, _| drop(view_2)); + view_1.update(app, |_, ctx| ctx.focus(&view_2)); + view_1.update(app, |_, ctx| ctx.focus(&view_1)); + view_1.update(app, |_, ctx| ctx.focus(&view_2)); + view_1.update(app, |_, _| drop(view_2)); - assert_eq!( - *events.lock(), - [ - "view 1 focused".to_string(), - "view 1 blurred".to_string(), - "view 2 focused".to_string(), - "view 2 blurred".to_string(), - "view 1 focused".to_string(), - "view 1 blurred".to_string(), - "view 2 focused".to_string(), - "view 1 focused".to_string(), - ], - ); - }) + assert_eq!( + *events.lock(), + [ + "view 1 focused".to_string(), + "view 1 blurred".to_string(), + "view 2 focused".to_string(), + "view 2 blurred".to_string(), + "view 1 focused".to_string(), + "view 1 blurred".to_string(), + "view 2 focused".to_string(), + "view 1 focused".to_string(), + ], + ); } - #[test] - fn test_spawn_from_view() { + #[crate::test(self)] + async fn test_spawn_from_view(mut app: TestAppContext) { #[derive(Default)] struct View { count: usize, @@ -3344,29 +3318,27 @@ mod tests { } } - App::test_async((), |mut app| async move { - let handle = app.add_window(|_| View::default()).1; - handle - .update(&mut app, |_, c| { - c.spawn(async { 7 }, |me, output, _| { - me.count = output; - }) + let handle = app.add_window(|_| View::default()).1; + handle + .update(&mut app, |_, c| { + c.spawn(async { 7 }, |me, output, _| { + me.count = output; }) - .await; - app.read(|ctx| assert_eq!(handle.read(ctx).count, 7)); - handle - .update(&mut app, |_, c| { - c.spawn(async { 14 }, |me, output, _| { - me.count = output; - }) + }) + .await; + app.read(|ctx| assert_eq!(handle.read(ctx).count, 7)); + handle + .update(&mut app, |_, c| { + c.spawn(async { 14 }, |me, output, _| { + me.count = output; }) - .await; - app.read(|ctx| assert_eq!(handle.read(ctx).count, 14)); - }); + }) + .await; + app.read(|ctx| assert_eq!(handle.read(ctx).count, 14)); } - #[test] - fn test_spawn_stream_local_from_view() { + #[crate::test(self)] + async fn test_spawn_stream_local_from_view(mut app: TestAppContext) { #[derive(Default)] struct View { events: Vec>, @@ -3386,28 +3358,26 @@ mod tests { } } - App::test_async((), |mut app| async move { - let (_, handle) = app.add_window(|_| View::default()); - handle - .update(&mut app, |_, c| { - c.spawn_stream( - smol::stream::iter(vec![1_usize, 2, 3]), - |me, output, _| { - me.events.push(Some(output)); - }, - |me, _| { - me.events.push(None); - }, - ) - }) - .await; + let (_, handle) = app.add_window(|_| View::default()); + handle + .update(&mut app, |_, c| { + c.spawn_stream( + smol::stream::iter(vec![1_usize, 2, 3]), + |me, output, _| { + me.events.push(Some(output)); + }, + |me, _| { + me.events.push(None); + }, + ) + }) + .await; - app.read(|ctx| assert_eq!(handle.read(ctx).events, [Some(1), Some(2), Some(3), None])) - }); + app.read(|ctx| assert_eq!(handle.read(ctx).events, [Some(1), Some(2), Some(3), None])) } - #[test] - fn test_dispatch_action() { + #[crate::test(self)] + fn test_dispatch_action(app: &mut MutableAppContext) { struct ViewA { id: usize, } @@ -3448,81 +3418,79 @@ mod tests { foo: String, } - App::test((), |app| { - let actions = Rc::new(RefCell::new(Vec::new())); + let actions = Rc::new(RefCell::new(Vec::new())); - let actions_clone = actions.clone(); - app.add_global_action("action", move |_: &ActionArg, _: &mut MutableAppContext| { - actions_clone.borrow_mut().push("global a".to_string()); - }); + let actions_clone = actions.clone(); + app.add_global_action("action", move |_: &ActionArg, _: &mut MutableAppContext| { + actions_clone.borrow_mut().push("global a".to_string()); + }); - let actions_clone = actions.clone(); - app.add_global_action("action", move |_: &ActionArg, _: &mut MutableAppContext| { - actions_clone.borrow_mut().push("global b".to_string()); - }); + let actions_clone = actions.clone(); + app.add_global_action("action", move |_: &ActionArg, _: &mut MutableAppContext| { + actions_clone.borrow_mut().push("global b".to_string()); + }); - let actions_clone = actions.clone(); - app.add_action("action", move |view: &mut ViewA, arg: &ActionArg, ctx| { - assert_eq!(arg.foo, "bar"); + let actions_clone = actions.clone(); + app.add_action("action", move |view: &mut ViewA, arg: &ActionArg, ctx| { + assert_eq!(arg.foo, "bar"); + ctx.propagate_action(); + actions_clone.borrow_mut().push(format!("{} a", view.id)); + }); + + let actions_clone = actions.clone(); + app.add_action("action", move |view: &mut ViewA, _: &ActionArg, ctx| { + if view.id != 1 { ctx.propagate_action(); - actions_clone.borrow_mut().push(format!("{} a", view.id)); - }); + } + actions_clone.borrow_mut().push(format!("{} b", view.id)); + }); - let actions_clone = actions.clone(); - app.add_action("action", move |view: &mut ViewA, _: &ActionArg, ctx| { - if view.id != 1 { - ctx.propagate_action(); - } - actions_clone.borrow_mut().push(format!("{} b", view.id)); - }); + let actions_clone = actions.clone(); + app.add_action("action", move |view: &mut ViewB, _: &ActionArg, ctx| { + ctx.propagate_action(); + actions_clone.borrow_mut().push(format!("{} c", view.id)); + }); - let actions_clone = actions.clone(); - app.add_action("action", move |view: &mut ViewB, _: &ActionArg, ctx| { - ctx.propagate_action(); - actions_clone.borrow_mut().push(format!("{} c", view.id)); - }); + let actions_clone = actions.clone(); + app.add_action("action", move |view: &mut ViewB, _: &ActionArg, ctx| { + ctx.propagate_action(); + actions_clone.borrow_mut().push(format!("{} d", view.id)); + }); - let actions_clone = actions.clone(); - app.add_action("action", move |view: &mut ViewB, _: &ActionArg, ctx| { - ctx.propagate_action(); - actions_clone.borrow_mut().push(format!("{} d", view.id)); - }); + let (window_id, view_1) = app.add_window(|_| ViewA { id: 1 }); + let view_2 = app.add_view(window_id, |_| ViewB { id: 2 }); + let view_3 = app.add_view(window_id, |_| ViewA { id: 3 }); + let view_4 = app.add_view(window_id, |_| ViewB { id: 4 }); - let (window_id, view_1) = app.add_window(|_| ViewA { id: 1 }); - let view_2 = app.add_view(window_id, |_| ViewB { id: 2 }); - let view_3 = app.add_view(window_id, |_| ViewA { id: 3 }); - let view_4 = app.add_view(window_id, |_| ViewB { id: 4 }); + app.dispatch_action( + window_id, + vec![view_1.id(), view_2.id(), view_3.id(), view_4.id()], + "action", + ActionArg { foo: "bar".into() }, + ); - app.dispatch_action( - window_id, - vec![view_1.id(), view_2.id(), view_3.id(), view_4.id()], - "action", - ActionArg { foo: "bar".into() }, - ); + assert_eq!( + *actions.borrow(), + vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "1 b"] + ); - assert_eq!( - *actions.borrow(), - vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "1 b"] - ); + // Remove view_1, which doesn't propagate the action + actions.borrow_mut().clear(); + app.dispatch_action( + window_id, + vec![view_2.id(), view_3.id(), view_4.id()], + "action", + ActionArg { foo: "bar".into() }, + ); - // Remove view_1, which doesn't propagate the action - actions.borrow_mut().clear(); - app.dispatch_action( - window_id, - vec![view_2.id(), view_3.id(), view_4.id()], - "action", - ActionArg { foo: "bar".into() }, - ); - - assert_eq!( - *actions.borrow(), - vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "global b", "global a"] - ); - }) + assert_eq!( + *actions.borrow(), + vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "global b", "global a"] + ); } - #[test] - fn test_dispatch_keystroke() { + #[crate::test(self)] + fn test_dispatch_keystroke(app: &mut MutableAppContext) { use std::cell::Cell; #[derive(Clone)] @@ -3562,45 +3530,43 @@ mod tests { } } - App::test((), |app| { - let mut view_1 = View::new(1); - let mut view_2 = View::new(2); - let mut view_3 = View::new(3); - view_1.keymap_context.set.insert("a".into()); - view_2.keymap_context.set.insert("b".into()); - view_3.keymap_context.set.insert("c".into()); + let mut view_1 = View::new(1); + let mut view_2 = View::new(2); + let mut view_3 = View::new(3); + view_1.keymap_context.set.insert("a".into()); + view_2.keymap_context.set.insert("b".into()); + view_3.keymap_context.set.insert("c".into()); - let (window_id, view_1) = app.add_window(|_| view_1); - let view_2 = app.add_view(window_id, |_| view_2); - let view_3 = app.add_view(window_id, |_| view_3); + let (window_id, view_1) = app.add_window(|_| view_1); + let view_2 = app.add_view(window_id, |_| view_2); + let view_3 = app.add_view(window_id, |_| view_3); - // This keymap's only binding dispatches an action on view 2 because that view will have - // "a" and "b" in its context, but not "c". - let binding = keymap::Binding::new("a", "action", Some("a && b && !c")) - .with_arg(ActionArg { key: "a".into() }); - app.add_bindings(vec![binding]); + // This keymap's only binding dispatches an action on view 2 because that view will have + // "a" and "b" in its context, but not "c". + let binding = keymap::Binding::new("a", "action", Some("a && b && !c")) + .with_arg(ActionArg { key: "a".into() }); + app.add_bindings(vec![binding]); - let handled_action = Rc::new(Cell::new(false)); - let handled_action_clone = handled_action.clone(); - app.add_action("action", move |view: &mut View, arg: &ActionArg, _ctx| { - handled_action_clone.set(true); - assert_eq!(view.id, 2); - assert_eq!(arg.key, "a"); - }); - - app.dispatch_keystroke( - window_id, - vec![view_1.id(), view_2.id(), view_3.id()], - &Keystroke::parse("a").unwrap(), - ) - .unwrap(); - - assert!(handled_action.get()); + let handled_action = Rc::new(Cell::new(false)); + let handled_action_clone = handled_action.clone(); + app.add_action("action", move |view: &mut View, arg: &ActionArg, _ctx| { + handled_action_clone.set(true); + assert_eq!(view.id, 2); + assert_eq!(arg.key, "a"); }); + + app.dispatch_keystroke( + window_id, + vec![view_1.id(), view_2.id(), view_3.id()], + &Keystroke::parse("a").unwrap(), + ) + .unwrap(); + + assert!(handled_action.get()); } - #[test] - fn test_model_condition() { + #[crate::test(self)] + async fn test_model_condition(mut app: TestAppContext) { struct Counter(usize); impl super::Entity for Counter { @@ -3614,62 +3580,56 @@ mod tests { } } - App::test_async((), |mut app| async move { - let model = app.add_model(|_| Counter(0)); + let model = app.add_model(|_| Counter(0)); - let condition1 = model.condition(&app, |model, _| model.0 == 2); - let condition2 = model.condition(&app, |model, _| model.0 == 3); - smol::pin!(condition1, condition2); + let condition1 = model.condition(&app, |model, _| model.0 == 2); + let condition2 = model.condition(&app, |model, _| model.0 == 3); + smol::pin!(condition1, condition2); - model.update(&mut app, |model, ctx| model.inc(ctx)); - assert_eq!(poll_once(&mut condition1).await, None); - assert_eq!(poll_once(&mut condition2).await, None); + model.update(&mut app, |model, ctx| model.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, None); + assert_eq!(poll_once(&mut condition2).await, None); - model.update(&mut app, |model, ctx| model.inc(ctx)); - assert_eq!(poll_once(&mut condition1).await, Some(())); - assert_eq!(poll_once(&mut condition2).await, None); + model.update(&mut app, |model, ctx| model.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, Some(())); + assert_eq!(poll_once(&mut condition2).await, None); - model.update(&mut app, |model, ctx| model.inc(ctx)); - assert_eq!(poll_once(&mut condition2).await, Some(())); + model.update(&mut app, |model, ctx| model.inc(ctx)); + assert_eq!(poll_once(&mut condition2).await, Some(())); - model.update(&mut app, |_, ctx| ctx.notify()); - }); + model.update(&mut app, |_, ctx| ctx.notify()); } - #[test] + #[crate::test(self)] #[should_panic] - fn test_model_condition_timeout() { + async fn test_model_condition_timeout(mut app: TestAppContext) { struct Model; impl super::Entity for Model { type Event = (); } - App::test_async((), |mut app| async move { - let model = app.add_model(|_| Model); - model.condition(&app, |_, _| false).await; - }); + let model = app.add_model(|_| Model); + model.condition(&app, |_, _| false).await; } - #[test] + #[crate::test(self)] #[should_panic(expected = "model dropped with pending condition")] - fn test_model_condition_panic_on_drop() { + async fn test_model_condition_panic_on_drop(mut app: TestAppContext) { struct Model; impl super::Entity for Model { type Event = (); } - App::test_async((), |mut app| async move { - let model = app.add_model(|_| Model); - let condition = model.condition(&app, |_, _| false); - app.update(|_| drop(model)); - condition.await; - }); + let model = app.add_model(|_| Model); + let condition = model.condition(&app, |_, _| false); + app.update(|_| drop(model)); + condition.await; } - #[test] - fn test_view_condition() { + #[crate::test(self)] + async fn test_view_condition(mut app: TestAppContext) { struct Counter(usize); impl super::Entity for Counter { @@ -3693,30 +3653,28 @@ mod tests { } } - App::test_async((), |mut app| async move { - let (_, view) = app.add_window(|_| Counter(0)); + let (_, view) = app.add_window(|_| Counter(0)); - let condition1 = view.condition(&app, |view, _| view.0 == 2); - let condition2 = view.condition(&app, |view, _| view.0 == 3); - smol::pin!(condition1, condition2); + let condition1 = view.condition(&app, |view, _| view.0 == 2); + let condition2 = view.condition(&app, |view, _| view.0 == 3); + smol::pin!(condition1, condition2); - view.update(&mut app, |view, ctx| view.inc(ctx)); - assert_eq!(poll_once(&mut condition1).await, None); - assert_eq!(poll_once(&mut condition2).await, None); + view.update(&mut app, |view, ctx| view.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, None); + assert_eq!(poll_once(&mut condition2).await, None); - view.update(&mut app, |view, ctx| view.inc(ctx)); - assert_eq!(poll_once(&mut condition1).await, Some(())); - assert_eq!(poll_once(&mut condition2).await, None); + view.update(&mut app, |view, ctx| view.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, Some(())); + assert_eq!(poll_once(&mut condition2).await, None); - view.update(&mut app, |view, ctx| view.inc(ctx)); - assert_eq!(poll_once(&mut condition2).await, Some(())); - view.update(&mut app, |_, ctx| ctx.notify()); - }); + view.update(&mut app, |view, ctx| view.inc(ctx)); + assert_eq!(poll_once(&mut condition2).await, Some(())); + view.update(&mut app, |_, ctx| ctx.notify()); } - #[test] + #[crate::test(self)] #[should_panic] - fn test_view_condition_timeout() { + async fn test_view_condition_timeout(mut app: TestAppContext) { struct View; impl super::Entity for View { @@ -3733,15 +3691,13 @@ mod tests { } } - App::test_async((), |mut app| async move { - let (_, view) = app.add_window(|_| View); - view.condition(&app, |_, _| false).await; - }); + let (_, view) = app.add_window(|_| View); + view.condition(&app, |_, _| false).await; } - #[test] + #[crate::test(self)] #[should_panic(expected = "view dropped with pending condition")] - fn test_view_condition_panic_on_drop() { + async fn test_view_condition_panic_on_drop(mut app: TestAppContext) { struct View; impl super::Entity for View { @@ -3758,17 +3714,15 @@ mod tests { } } - App::test_async((), |mut app| async move { - let window_id = app.add_window(|_| View).0; - let view = app.add_view(window_id, |_| View); + let window_id = app.add_window(|_| View).0; + let view = app.add_view(window_id, |_| View); - let condition = view.condition(&app, |_, _| false); - app.update(|_| drop(view)); - condition.await; - }); + let condition = view.condition(&app, |_, _| false); + app.update(|_| drop(view)); + condition.await; } - // #[test] + // #[crate::test(self)] // fn test_ui_and_window_updates() { // struct View { // count: usize, diff --git a/gpui_macros/src/lib.rs b/gpui_macros/src/lib.rs index 1045868777..cdd4cacb80 100644 --- a/gpui_macros/src/lib.rs +++ b/gpui_macros/src/lib.rs @@ -2,38 +2,56 @@ use std::mem; use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{parse_macro_input, ItemFn}; +use syn::{parse_macro_input, parse_quote, AttributeArgs, ItemFn, Meta, NestedMeta}; #[proc_macro_attribute] pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { - assert!(args.is_empty()); + let mut namespace = format_ident!("gpui"); + + let args = syn::parse_macro_input!(args as AttributeArgs); + for arg in args { + match arg { + NestedMeta::Meta(Meta::Path(name)) + if name.get_ident().map_or(false, |n| n == "self") => + { + namespace = format_ident!("crate"); + } + other => { + return TokenStream::from( + syn::Error::new_spanned(other, "invalid argument").into_compile_error(), + ) + } + } + } let mut inner_fn = parse_macro_input!(function as ItemFn); + let inner_fn_attributes = mem::take(&mut inner_fn.attrs); let inner_fn_name = format_ident!("_{}", inner_fn.sig.ident); let outer_fn_name = mem::replace(&mut inner_fn.sig.ident, inner_fn_name.clone()); - let outer_fn = if inner_fn.sig.asyncness.is_some() { - quote! { + let mut outer_fn: ItemFn = if inner_fn.sig.asyncness.is_some() { + parse_quote! { #[test] fn #outer_fn_name() { #inner_fn - gpui::App::test_async((), move |ctx| async { + #namespace::App::test_async((), move |ctx| async { #inner_fn_name(ctx).await; }); } } } else { - quote! { + parse_quote! { #[test] fn #outer_fn_name() { #inner_fn - gpui::App::test((), |ctx| { + #namespace::App::test((), |ctx| { #inner_fn_name(ctx); }); } } }; + outer_fn.attrs.extend(inner_fn_attributes); - TokenStream::from(outer_fn) + TokenStream::from(quote!(#outer_fn)) } From fa6bd1f9263a9d1b5418b43675f7d453d398d779 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 12 May 2021 15:16:49 -0600 Subject: [PATCH 5/7] Introduce AsyncAppContext and simplify spawning Now when you call spawn in various context, you pass an FnOnce that is called with an AsyncAppContext and returns a static future. This allows you to write async code similar to how our tests work, borrowing the guts of the AsyncAppContext when needed to interact, but using normal async await semantics instead of callbacks. Co-Authored-By: Max Brunsfeld --- gpui/src/app.rs | 698 +++++++++++++--------------------- zed/src/editor/buffer/mod.rs | 30 +- zed/src/editor/buffer_view.rs | 31 +- zed/src/file_finder.rs | 9 +- zed/src/workspace.rs | 96 ++--- zed/src/worktree.rs | 49 ++- 6 files changed, 388 insertions(+), 525 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 446220773a..3bcccef47c 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -8,6 +8,7 @@ use crate::{ AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache, }; use anyhow::{anyhow, Result}; +use async_task::Task; use keymap::MatchResult; use parking_lot::{Mutex, RwLock}; use pathfinder_geometry::{rect::RectF, vector::vec2f}; @@ -50,6 +51,14 @@ pub trait ReadModel { fn read_model(&self, handle: &ModelHandle) -> &T; } +pub trait ReadModelWith { + fn read_model_with T, T>( + &self, + handle: &ModelHandle, + read: F, + ) -> T; +} + pub trait UpdateModel { fn update_model(&mut self, handle: &ModelHandle, update: F) -> S where @@ -61,6 +70,13 @@ pub trait ReadView { fn read_view(&self, handle: &ViewHandle) -> &T; } +pub trait ReadViewWith { + fn read_view_with(&self, handle: &ViewHandle, read: F) -> T + where + V: View, + F: FnOnce(&V, &AppContext) -> T; +} + pub trait UpdateView { fn update_view(&mut self, handle: &ViewHandle, update: F) -> S where @@ -86,6 +102,8 @@ pub enum MenuItem<'a> { #[derive(Clone)] pub struct App(Rc>); +pub struct AsyncAppContext(Rc>); + #[derive(Clone)] pub struct TestAppContext(Rc>, Rc); @@ -345,6 +363,80 @@ impl TestAppContext { } } +impl AsyncAppContext { + pub fn read T>(&mut self, callback: F) -> T { + callback(self.0.borrow().as_ref()) + } + + pub fn update T>(&mut self, callback: F) -> T { + let mut state = self.0.borrow_mut(); + state.pending_flushes += 1; + let result = callback(&mut *state); + state.flush_effects(); + result + } + + pub fn add_model(&mut self, build_model: F) -> ModelHandle + where + T: Entity, + F: FnOnce(&mut ModelContext) -> T, + { + self.update(|ctx| ctx.add_model(build_model)) + } +} + +impl UpdateModel for AsyncAppContext { + fn update_model(&mut self, handle: &ModelHandle, update: F) -> S + where + T: Entity, + F: FnOnce(&mut T, &mut ModelContext) -> S, + { + let mut state = self.0.borrow_mut(); + state.pending_flushes += 1; + let result = state.update_model(handle, update); + state.flush_effects(); + result + } +} + +impl ReadModelWith for AsyncAppContext { + fn read_model_with T, T>( + &self, + handle: &ModelHandle, + read: F, + ) -> T { + let ctx = self.0.borrow(); + let ctx = ctx.as_ref(); + read(handle.read(ctx), ctx) + } +} + +impl UpdateView for AsyncAppContext { + fn update_view(&mut self, handle: &ViewHandle, update: F) -> S + where + T: View, + F: FnOnce(&mut T, &mut ViewContext) -> S, + { + let mut state = self.0.borrow_mut(); + state.pending_flushes += 1; + let result = state.update_view(handle, update); + state.flush_effects(); + result + } +} + +impl ReadViewWith for AsyncAppContext { + fn read_view_with(&self, handle: &ViewHandle, read: F) -> T + where + V: View, + F: FnOnce(&V, &AppContext) -> T, + { + let ctx = self.0.borrow(); + let ctx = ctx.as_ref(); + read(handle.read(ctx), ctx) + } +} + impl UpdateModel for TestAppContext { fn update_model(&mut self, handle: &ModelHandle, update: F) -> S where @@ -359,6 +451,18 @@ impl UpdateModel for TestAppContext { } } +impl ReadModelWith for TestAppContext { + fn read_model_with T, T>( + &self, + handle: &ModelHandle, + read: F, + ) -> T { + let ctx = self.0.borrow(); + let ctx = ctx.as_ref(); + read(handle.read(ctx), ctx) + } +} + impl UpdateView for TestAppContext { fn update_view(&mut self, handle: &ViewHandle, update: F) -> S where @@ -373,6 +477,18 @@ impl UpdateView for TestAppContext { } } +impl ReadViewWith for TestAppContext { + fn read_view_with(&self, handle: &ViewHandle, read: F) -> T + where + V: View, + F: FnOnce(&V, &AppContext) -> T, + { + let ctx = self.0.borrow(); + let ctx = ctx.as_ref(); + read(handle.read(ctx), ctx) + } +} + type ActionCallback = dyn FnMut(&mut dyn AnyView, &dyn Any, &mut MutableAppContext, usize, usize) -> bool; @@ -388,7 +504,6 @@ pub struct MutableAppContext { keystroke_matcher: keymap::Matcher, next_entity_id: usize, next_window_id: usize, - next_task_id: usize, subscriptions: HashMap>, model_observations: HashMap>, view_observations: HashMap>, @@ -396,8 +511,6 @@ pub struct MutableAppContext { HashMap>, Box)>, debug_elements_callbacks: HashMap crate::json::Value>>, foreground: Rc, - future_handlers: Rc>>, - stream_handlers: Rc>>, pending_effects: VecDeque, pending_flushes: usize, flushing_effects: bool, @@ -429,15 +542,12 @@ impl MutableAppContext { keystroke_matcher: keymap::Matcher::default(), next_entity_id: 0, next_window_id: 0, - next_task_id: 0, subscriptions: HashMap::new(), model_observations: HashMap::new(), view_observations: HashMap::new(), presenters_and_platform_windows: HashMap::new(), debug_elements_callbacks: HashMap::new(), foreground, - future_handlers: Default::default(), - stream_handlers: Default::default(), pending_effects: VecDeque::new(), pending_flushes: 0, flushing_effects: false, @@ -1159,96 +1269,14 @@ impl MutableAppContext { self.flush_effects(); } - fn spawn(&mut self, spawner: Spawner, future: F) -> EntityTask + pub fn spawn(&self, f: F) -> Task where - F: 'static + Future, + F: FnOnce(AsyncAppContext) -> Fut, + Fut: 'static + Future, T: 'static, { - let task_id = post_inc(&mut self.next_task_id); - let app = self.weak_self.as_ref().unwrap().upgrade().unwrap(); - let task = { - let app = app.clone(); - self.foreground.spawn(async move { - let output = future.await; - app.borrow_mut() - .handle_future_output(task_id, Box::new(output)) - .map(|output| *output.downcast::().unwrap()) - }) - }; - EntityTask::new( - task_id, - task, - spawner, - TaskHandlerMap::Future(self.future_handlers.clone()), - ) - } - - fn spawn_stream(&mut self, spawner: Spawner, mut stream: F) -> EntityTask - where - F: 'static + Stream + Unpin, - T: 'static, - { - let task_id = post_inc(&mut self.next_task_id); - let app = self.weak_self.as_ref().unwrap().upgrade().unwrap(); - let task = self.foreground.spawn(async move { - loop { - match stream.next().await { - Some(item) => { - let mut app = app.borrow_mut(); - if app.handle_stream_item(task_id, Box::new(item)) { - break; - } - } - None => { - break; - } - } - } - - app.borrow_mut() - .stream_completed(task_id) - .map(|output| *output.downcast::().unwrap()) - }); - - EntityTask::new( - task_id, - task, - spawner, - TaskHandlerMap::Stream(self.stream_handlers.clone()), - ) - } - - fn handle_future_output( - &mut self, - task_id: usize, - output: Box, - ) -> Option> { - self.pending_flushes += 1; - let future_callback = self.future_handlers.borrow_mut().remove(&task_id).unwrap(); - let result = future_callback(output, self); - self.flush_effects(); - result - } - - fn handle_stream_item(&mut self, task_id: usize, output: Box) -> bool { - self.pending_flushes += 1; - - let mut handler = self.stream_handlers.borrow_mut().remove(&task_id).unwrap(); - let halt = (handler.item_callback)(output, self); - self.stream_handlers.borrow_mut().insert(task_id, handler); - - self.flush_effects(); - halt - } - - fn stream_completed(&mut self, task_id: usize) -> Option> { - self.pending_flushes += 1; - - let handler = self.stream_handlers.borrow_mut().remove(&task_id).unwrap(); - let result = (handler.done_callback)(self); - - self.flush_effects(); - result + let ctx = AsyncAppContext(self.weak_self.as_ref().unwrap().upgrade().unwrap()); + self.foreground.spawn(f(ctx)) } pub fn write_to_clipboard(&self, item: ClipboardItem) { @@ -1624,76 +1652,13 @@ impl<'a, T: Entity> ModelContext<'a, T> { ModelHandle::new(self.model_id, &self.app.ctx.ref_counts) } - pub fn spawn(&mut self, future: S, callback: F) -> EntityTask + pub fn spawn(&self, f: F) -> Task where - S: 'static + Future, - F: 'static + FnOnce(&mut T, S::Output, &mut ModelContext) -> U, - U: 'static, + F: FnOnce(AsyncAppContext) -> Fut, + Fut: 'static + Future, + S: 'static, { - let handle = self.handle(); - let weak_handle = handle.downgrade(); - let task = self - .app - .spawn::(Spawner::Model(handle.into()), future); - - self.app.future_handlers.borrow_mut().insert( - task.id, - Box::new(move |output, ctx| { - weak_handle.upgrade(ctx.as_ref()).map(|handle| { - let output = *output.downcast().unwrap(); - handle.update(ctx, |model, ctx| { - Box::new(callback(model, output, ctx)) as Box - }) - }) - }), - ); - - task - } - - pub fn spawn_stream( - &mut self, - stream: S, - mut item_callback: F, - done_callback: G, - ) -> EntityTask - where - S: 'static + Stream + Unpin, - F: 'static + FnMut(&mut T, S::Item, &mut ModelContext), - G: 'static + FnOnce(&mut T, &mut ModelContext) -> U, - U: 'static + Any, - { - let handle = self.handle(); - let weak_handle = handle.downgrade(); - let task = self.app.spawn_stream(Spawner::Model(handle.into()), stream); - - self.app.stream_handlers.borrow_mut().insert( - task.id, - StreamHandler { - item_callback: { - let weak_handle = weak_handle.clone(); - Box::new(move |output, app| { - if let Some(handle) = weak_handle.upgrade(app.as_ref()) { - let output = *output.downcast().unwrap(); - handle.update(app, |model, ctx| { - item_callback(model, output, ctx); - ctx.halt_stream - }) - } else { - true - } - }) - }, - done_callback: Box::new(move |app| { - weak_handle.upgrade(app.as_ref()).map(|handle| { - handle.update(app, |model, ctx| Box::new(done_callback(model, ctx))) - as Box - }) - }), - }, - ); - - task + self.app.spawn(f) } } @@ -1731,7 +1696,6 @@ pub struct ViewContext<'a, T: ?Sized> { view_id: usize, view_type: PhantomData, halt_action_dispatch: bool, - halt_stream: bool, } impl<'a, T: View> ViewContext<'a, T> { @@ -1742,7 +1706,6 @@ impl<'a, T: View> ViewContext<'a, T> { view_id, view_type: PhantomData, halt_action_dispatch: true, - halt_stream: false, } } @@ -1944,77 +1907,13 @@ impl<'a, T: View> ViewContext<'a, T> { self.halt_action_dispatch = false; } - pub fn halt_stream(&mut self) { - self.halt_stream = true; - } - - pub fn spawn(&mut self, future: S, callback: F) -> EntityTask + pub fn spawn(&self, f: F) -> Task where - S: 'static + Future, - F: 'static + FnOnce(&mut T, S::Output, &mut ViewContext) -> U, - U: 'static, + F: FnOnce(AsyncAppContext) -> Fut, + Fut: 'static + Future, + S: 'static, { - let handle = self.handle(); - let weak_handle = handle.downgrade(); - let task = self.app.spawn(Spawner::View(handle.into()), future); - - self.app.future_handlers.borrow_mut().insert( - task.id, - Box::new(move |output, app| { - weak_handle.upgrade(app.as_ref()).map(|handle| { - let output = *output.downcast().unwrap(); - handle.update(app, |view, ctx| { - Box::new(callback(view, output, ctx)) as Box - }) - }) - }), - ); - - task - } - - pub fn spawn_stream( - &mut self, - stream: S, - mut item_callback: F, - done_callback: G, - ) -> EntityTask - where - S: 'static + Stream + Unpin, - F: 'static + FnMut(&mut T, S::Item, &mut ViewContext), - G: 'static + FnOnce(&mut T, &mut ViewContext) -> U, - U: 'static + Any, - { - let handle = self.handle(); - let weak_handle = handle.downgrade(); - let task = self.app.spawn_stream(Spawner::View(handle.into()), stream); - self.app.stream_handlers.borrow_mut().insert( - task.id, - StreamHandler { - item_callback: { - let weak_handle = weak_handle.clone(); - Box::new(move |output, ctx| { - if let Some(handle) = weak_handle.upgrade(ctx.as_ref()) { - let output = *output.downcast().unwrap(); - handle.update(ctx, |view, ctx| { - item_callback(view, output, ctx); - ctx.halt_stream - }) - } else { - true - } - }) - }, - done_callback: Box::new(move |ctx| { - weak_handle.upgrade(ctx.as_ref()).map(|handle| { - handle.update(ctx, |view, ctx| { - Box::new(done_callback(view, ctx)) as Box - }) - }) - }), - }, - ); - task + self.app.spawn(f) } } @@ -2107,6 +2006,14 @@ impl ModelHandle { app.read_model(self) } + pub fn read_with<'a, A, F, S>(&self, ctx: &A, read: F) -> S + where + A: ReadModelWith, + F: FnOnce(&T, &AppContext) -> S, + { + ctx.read_model_with(self, read) + } + pub fn update(&self, app: &mut A, update: F) -> S where A: UpdateModel, @@ -2249,9 +2156,10 @@ impl WeakModelHandle { } } - pub fn upgrade(&self, app: &AppContext) -> Option> { - if app.models.contains_key(&self.model_id) { - Some(ModelHandle::new(self.model_id, &app.ref_counts)) + pub fn upgrade(&self, ctx: impl AsRef) -> Option> { + let ctx = ctx.as_ref(); + if ctx.models.contains_key(&self.model_id) { + Some(ModelHandle::new(self.model_id, &ctx.ref_counts)) } else { None } @@ -2301,6 +2209,14 @@ impl ViewHandle { app.read_view(self) } + pub fn read_with(&self, ctx: &A, read: F) -> S + where + A: ReadViewWith, + F: FnOnce(&T, &AppContext) -> S, + { + ctx.read_view_with(self, read) + } + pub fn update(&self, app: &mut A, update: F) -> S where A: UpdateView, @@ -2711,86 +2627,6 @@ struct ViewObservation { callback: Box, } -type FutureHandler = Box, &mut MutableAppContext) -> Option>>; - -struct StreamHandler { - item_callback: Box, &mut MutableAppContext) -> bool>, - done_callback: Box Option>>, -} - -#[must_use] -pub struct EntityTask { - id: usize, - task: Option>>, - _spawner: Spawner, // Keeps the spawning entity alive for as long as the task exists - handler_map: TaskHandlerMap, -} - -pub enum Spawner { - Model(AnyModelHandle), - View(AnyViewHandle), -} - -enum TaskHandlerMap { - Detached, - Future(Rc>>), - Stream(Rc>>), -} - -impl EntityTask { - fn new( - id: usize, - task: executor::Task>, - spawner: Spawner, - handler_map: TaskHandlerMap, - ) -> Self { - Self { - id, - task: Some(task), - _spawner: spawner, - handler_map, - } - } - - pub fn detach(mut self) { - self.handler_map = TaskHandlerMap::Detached; - self.task.take().unwrap().detach(); - } - - pub async fn cancel(mut self) -> Option { - let task = self.task.take().unwrap(); - task.cancel().await.unwrap() - } -} - -impl Future for EntityTask { - type Output = T; - - fn poll( - self: std::pin::Pin<&mut Self>, - ctx: &mut std::task::Context<'_>, - ) -> std::task::Poll { - let task = unsafe { self.map_unchecked_mut(|task| task.task.as_mut().unwrap()) }; - task.poll(ctx).map(|output| output.unwrap()) - } -} - -impl Drop for EntityTask { - fn drop(self: &mut Self) { - match &self.handler_map { - TaskHandlerMap::Detached => { - return; - } - TaskHandlerMap::Future(map) => { - map.borrow_mut().remove(&self.id); - } - TaskHandlerMap::Stream(map) => { - map.borrow_mut().remove(&self.id); - } - } - } -} - #[cfg(test)] mod tests { use super::*; @@ -2926,60 +2762,60 @@ mod tests { assert_eq!(handle_1.read(app).events, vec![7, 10, 5]) } - #[crate::test(self)] - async fn test_spawn_from_model(mut app: TestAppContext) { - #[derive(Default)] - struct Model { - count: usize, - } + // #[crate::test(self)] + // async fn test_spawn_from_model(mut app: TestAppContext) { + // #[derive(Default)] + // struct Model { + // count: usize, + // } - impl Entity for Model { - type Event = (); - } + // impl Entity for Model { + // type Event = (); + // } - let handle = app.add_model(|_| Model::default()); - handle - .update(&mut app, |_, c| { - c.spawn(async { 7 }, |model, output, _| { - model.count = output; - }) - }) - .await; - app.read(|ctx| assert_eq!(handle.read(ctx).count, 7)); + // let handle = app.add_model(|_| Model::default()); + // handle + // .update(&mut app, |_, c| { + // c.spawn(async { 7 }, |model, output, _| { + // model.count = output; + // }) + // }) + // .await; + // app.read(|ctx| assert_eq!(handle.read(ctx).count, 7)); - handle - .update(&mut app, |_, c| { - c.spawn(async { 14 }, |model, output, _| { - model.count = output; - }) - }) - .await; - app.read(|ctx| assert_eq!(handle.read(ctx).count, 14)); - } + // handle + // .update(&mut app, |_, c| { + // c.spawn(async { 14 }, |model, output, _| { + // model.count = output; + // }) + // }) + // .await; + // app.read(|ctx| assert_eq!(handle.read(ctx).count, 14)); + // } - #[crate::test(self)] - async fn test_spawn_stream_local_from_model(mut app: TestAppContext) { - #[derive(Default)] - struct Model { - events: Vec>, - } + // #[crate::test(self)] + // async fn test_spawn_stream_local_from_model(mut app: TestAppContext) { + // #[derive(Default)] + // struct Model { + // events: Vec>, + // } - impl Entity for Model { - type Event = (); - } + // impl Entity for Model { + // type Event = (); + // } - let handle = app.add_model(|_| Model::default()); - handle - .update(&mut app, |_, c| { - c.spawn_stream( - smol::stream::iter(vec![1, 2, 3]), - |model, output, _| model.events.push(Some(output)), - |model, _| model.events.push(None), - ) - }) - .await; - app.read(|ctx| assert_eq!(handle.read(ctx).events, [Some(1), Some(2), Some(3), None])); - } + // let handle = app.add_model(|_| Model::default()); + // handle + // .update(&mut app, |_, c| { + // c.spawn_stream( + // smol::stream::iter(vec![1, 2, 3]), + // |model, output, _| model.events.push(Some(output)), + // |model, _| model.events.push(None), + // ) + // }) + // .await; + // app.read(|ctx| assert_eq!(handle.read(ctx).events, [Some(1), Some(2), Some(3), None])); + // } #[crate::test(self)] fn test_view_handles(app: &mut MutableAppContext) { @@ -3297,84 +3133,84 @@ mod tests { ); } - #[crate::test(self)] - async fn test_spawn_from_view(mut app: TestAppContext) { - #[derive(Default)] - struct View { - count: usize, - } + // #[crate::test(self)] + // async fn test_spawn_from_view(mut app: TestAppContext) { + // #[derive(Default)] + // struct View { + // count: usize, + // } - impl Entity for View { - type Event = (); - } + // impl Entity for View { + // type Event = (); + // } - impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { - Empty::new().boxed() - } + // impl super::View for View { + // fn render<'a>(&self, _: &AppContext) -> ElementBox { + // Empty::new().boxed() + // } - fn ui_name() -> &'static str { - "View" - } - } + // fn ui_name() -> &'static str { + // "View" + // } + // } - let handle = app.add_window(|_| View::default()).1; - handle - .update(&mut app, |_, c| { - c.spawn(async { 7 }, |me, output, _| { - me.count = output; - }) - }) - .await; - app.read(|ctx| assert_eq!(handle.read(ctx).count, 7)); - handle - .update(&mut app, |_, c| { - c.spawn(async { 14 }, |me, output, _| { - me.count = output; - }) - }) - .await; - app.read(|ctx| assert_eq!(handle.read(ctx).count, 14)); - } + // let handle = app.add_window(|_| View::default()).1; + // handle + // .update(&mut app, |_, c| { + // c.spawn(async { 7 }, |me, output, _| { + // me.count = output; + // }) + // }) + // .await; + // app.read(|ctx| assert_eq!(handle.read(ctx).count, 7)); + // handle + // .update(&mut app, |_, c| { + // c.spawn(async { 14 }, |me, output, _| { + // me.count = output; + // }) + // }) + // .await; + // app.read(|ctx| assert_eq!(handle.read(ctx).count, 14)); + // } - #[crate::test(self)] - async fn test_spawn_stream_local_from_view(mut app: TestAppContext) { - #[derive(Default)] - struct View { - events: Vec>, - } + // #[crate::test(self)] + // async fn test_spawn_stream_local_from_view(mut app: TestAppContext) { + // #[derive(Default)] + // struct View { + // events: Vec>, + // } - impl Entity for View { - type Event = (); - } + // impl Entity for View { + // type Event = (); + // } - impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { - Empty::new().boxed() - } + // impl super::View for View { + // fn render<'a>(&self, _: &AppContext) -> ElementBox { + // Empty::new().boxed() + // } - fn ui_name() -> &'static str { - "View" - } - } + // fn ui_name() -> &'static str { + // "View" + // } + // } - let (_, handle) = app.add_window(|_| View::default()); - handle - .update(&mut app, |_, c| { - c.spawn_stream( - smol::stream::iter(vec![1_usize, 2, 3]), - |me, output, _| { - me.events.push(Some(output)); - }, - |me, _| { - me.events.push(None); - }, - ) - }) - .await; + // let (_, handle) = app.add_window(|_| View::default()); + // handle + // .update(&mut app, |_, c| { + // c.spawn_stream( + // smol::stream::iter(vec![1_usize, 2, 3]), + // |me, output, _| { + // me.events.push(Some(output)); + // }, + // |me, _| { + // me.events.push(None); + // }, + // ) + // }) + // .await; - app.read(|ctx| assert_eq!(handle.read(ctx).events, [Some(1), Some(2), Some(3), None])) - } + // app.read(|ctx| assert_eq!(handle.read(ctx).events, [Some(1), Some(2), Some(3), None])) + // } #[crate::test(self)] fn test_dispatch_action(app: &mut MutableAppContext) { diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 058fb03806..5d039fdc99 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -4,11 +4,9 @@ mod selection; mod text; pub use anchor::*; -use futures_core::future::LocalBoxFuture; pub use point::*; use seahash::SeaHasher; pub use selection::*; -use smol::future::FutureExt; pub use text::*; use crate::{ @@ -19,7 +17,7 @@ use crate::{ worktree::FileHandle, }; use anyhow::{anyhow, Result}; -use gpui::{Entity, ModelContext}; +use gpui::{Entity, ModelContext, Task}; use lazy_static::lazy_static; use rand::prelude::*; use std::{ @@ -475,21 +473,23 @@ impl Buffer { &mut self, new_file: Option, ctx: &mut ModelContext, - ) -> LocalBoxFuture<'static, Result<()>> { + ) -> Task> { let snapshot = self.snapshot(); let version = self.version.clone(); - if let Some(file) = new_file.as_ref().or(self.file.as_ref()) { - let save_task = file.save(snapshot, ctx.as_ref()); - ctx.spawn(save_task, |me, save_result, ctx| { - if save_result.is_ok() { - me.did_save(version, new_file, ctx); + let file = self.file.clone(); + let handle = ctx.handle(); + + ctx.spawn(|mut ctx| async move { + if let Some(file) = new_file.as_ref().or(file.as_ref()) { + let result = ctx.read(|ctx| file.save(snapshot, ctx.as_ref())).await; + if result.is_ok() { + handle.update(&mut ctx, |me, ctx| me.did_save(version, new_file, ctx)); } - save_result - }) - .boxed_local() - } else { - async { Ok(()) }.boxed_local() - } + result + } else { + Ok(()) + } + }) } fn did_save( diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 2e54336e68..cc22d8dcc8 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -4,11 +4,10 @@ use super::{ }; use crate::{settings::Settings, util::post_inc, workspace, worktree::FileHandle}; use anyhow::Result; -use futures_core::future::LocalBoxFuture; use gpui::{ fonts::Properties as FontProperties, geometry::vector::Vector2F, keymap::Binding, text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, FontCache, ModelHandle, - MutableAppContext, TextLayoutCache, View, ViewContext, WeakViewHandle, + MutableAppContext, Task, TextLayoutCache, View, ViewContext, WeakViewHandle, }; use parking_lot::Mutex; use postage::watch; @@ -2348,13 +2347,13 @@ impl BufferView { ctx.notify(); let epoch = self.next_blink_epoch(); - ctx.spawn( - async move { - Timer::after(CURSOR_BLINK_INTERVAL).await; - epoch - }, - Self::resume_cursor_blinking, - ) + let handle = ctx.handle(); + ctx.spawn(|mut ctx| async move { + Timer::after(CURSOR_BLINK_INTERVAL).await; + handle.update(&mut ctx, |this, ctx| { + this.resume_cursor_blinking(epoch, ctx); + }) + }) .detach(); } @@ -2371,13 +2370,11 @@ impl BufferView { ctx.notify(); let epoch = self.next_blink_epoch(); - ctx.spawn( - async move { - Timer::after(CURSOR_BLINK_INTERVAL).await; - epoch - }, - Self::blink_cursors, - ) + let handle = ctx.handle(); + ctx.spawn(|mut ctx| async move { + Timer::after(CURSOR_BLINK_INTERVAL).await; + handle.update(&mut ctx, |this, ctx| this.blink_cursors(epoch, ctx)); + }) .detach(); } } @@ -2498,7 +2495,7 @@ impl workspace::ItemView for BufferView { &mut self, new_file: Option, ctx: &mut ViewContext, - ) -> LocalBoxFuture<'static, Result<()>> { + ) -> Task> { self.buffer.update(ctx, |b, ctx| b.save(new_file, ctx)) } diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index e2427adddd..0dc96e52fc 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -399,7 +399,7 @@ impl FileFinder { self.cancel_flag.store(true, atomic::Ordering::Relaxed); self.cancel_flag = Arc::new(AtomicBool::new(false)); let cancel_flag = self.cancel_flag.clone(); - let task = ctx.background_executor().spawn(async move { + let background_task = ctx.background_executor().spawn(async move { let include_root_name = snapshots.len() > 1; let matches = match_paths( snapshots.iter(), @@ -415,7 +415,12 @@ impl FileFinder { (search_id, did_cancel, query, matches) }); - ctx.spawn(task, Self::update_matches).detach(); + let handle = ctx.handle(); + ctx.spawn(|mut ctx| async move { + let matches = background_task.await; + handle.update(&mut ctx, |this, ctx| this.update_matches(matches, ctx)); + }) + .detach(); Some(()) } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 90f22eaba9..81e5b345ae 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -6,10 +6,10 @@ use crate::{ time::ReplicaId, worktree::{FileHandle, Worktree, WorktreeHandle}, }; -use futures_core::{future::LocalBoxFuture, Future}; +use futures_core::Future; use gpui::{ color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, - ClipboardItem, Entity, EntityTask, ModelHandle, MutableAppContext, PathPromptOptions, View, + ClipboardItem, Entity, ModelHandle, MutableAppContext, PathPromptOptions, Task, View, ViewContext, ViewHandle, WeakModelHandle, }; use log::error; @@ -123,7 +123,7 @@ pub trait ItemView: View { &mut self, _: Option, _: &mut ViewContext, - ) -> LocalBoxFuture<'static, anyhow::Result<()>>; + ) -> Task>; fn should_activate_item_on_event(_: &Self::Event) -> bool { false } @@ -161,7 +161,7 @@ pub trait ItemViewHandle: Send + Sync { &self, file: Option, ctx: &mut MutableAppContext, - ) -> LocalBoxFuture<'static, anyhow::Result<()>>; + ) -> Task>; } impl ItemHandle for ModelHandle { @@ -239,7 +239,7 @@ impl ItemViewHandle for ViewHandle { &self, file: Option, ctx: &mut MutableAppContext, - ) -> LocalBoxFuture<'static, anyhow::Result<()>> { + ) -> Task> { self.update(ctx, |item, ctx| item.save(file, ctx)) } @@ -359,16 +359,18 @@ impl Workspace { .cloned() .zip(entries.into_iter()) .map(|(abs_path, file)| { - ctx.spawn( - bg.spawn(async move { abs_path.is_file() }), - move |me, is_file, ctx| { + let handle = ctx.handle(); + let is_file = bg.spawn(async move { abs_path.is_file() }); + ctx.spawn(|mut ctx| async move { + let is_file = is_file.await; + handle.update(&mut ctx, |me, ctx| { if is_file { me.open_entry(file.entry_id(), ctx) } else { None } - }, - ) + }) + }) }) .collect::>(); async move { @@ -442,7 +444,7 @@ impl Workspace { &mut self, entry: (usize, Arc), ctx: &mut ViewContext, - ) -> Option> { + ) -> Option> { // If the active pane contains a view for this file, then activate // that item view. if self @@ -496,28 +498,31 @@ impl Workspace { let history = ctx .background_executor() .spawn(file.load_history(ctx.as_ref())); - ctx.spawn(history, move |_, history, ctx| { - *tx.borrow_mut() = Some(match history { - Ok(history) => Ok(Box::new(ctx.add_model(|ctx| { - Buffer::from_history(replica_id, history, Some(file), ctx) - }))), - Err(error) => Err(Arc::new(error)), + + ctx.as_mut() + .spawn(|mut ctx| async move { + *tx.borrow_mut() = Some(match history.await { + Ok(history) => Ok(Box::new(ctx.add_model(|ctx| { + Buffer::from_history(replica_id, history, Some(file), ctx) + }))), + Err(error) => Err(Arc::new(error)), + }) }) - }) - .detach() + .detach(); } let mut watch = self.loading_items.get(&entry).unwrap().clone(); - Some(ctx.spawn( - async move { - loop { - if let Some(load_result) = watch.borrow().as_ref() { - return load_result.clone(); - } - watch.next().await; + + let handle = ctx.handle(); + Some(ctx.spawn(|mut ctx| async move { + let load_result = loop { + if let Some(load_result) = watch.borrow().as_ref() { + break load_result.clone(); } - }, - move |me, load_result, ctx| { + watch.next().await; + }; + + handle.update(&mut ctx, |me, ctx| { me.loading_items.remove(&entry); match load_result { Ok(item) => { @@ -532,8 +537,8 @@ impl Workspace { log::error!("error opening item: {}", error); } } - }, - )) + }) + })) } pub fn active_item(&self, ctx: &ViewContext) -> Option> { @@ -552,28 +557,27 @@ impl Workspace { .to_path_buf(); ctx.prompt_for_new_path(&start_path, move |path, ctx| { if let Some(path) = path { - handle.update(ctx, move |this, ctx| { - let file = this.file_for_path(&path, ctx); - let task = item.save(Some(file), ctx.as_mut()); - ctx.spawn(task, move |_, result, _| { - if let Err(e) = result { - error!("failed to save item: {:?}, ", e); - } - }) - .detach() + ctx.spawn(|mut ctx| async move { + let file = + handle.update(&mut ctx, |me, ctx| me.file_for_path(&path, ctx)); + if let Err(error) = ctx.update(|ctx| item.save(Some(file), ctx)).await { + error!("failed to save item: {:?}, ", error); + } }) + .detach() } }); return; } - let task = item.save(None, ctx.as_mut()); - ctx.spawn(task, |_, result, _| { - if let Err(e) = result { - error!("failed to save item: {:?}, ", e); - } - }) - .detach() + let save = item.save(None, ctx.as_mut()); + ctx.foreground() + .spawn(async move { + if let Err(e) = save.await { + error!("failed to save item: {:?}, ", e); + } + }) + .detach(); } } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 30383f5a0a..55d8f91b99 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -16,7 +16,7 @@ use postage::{ prelude::{Sink, Stream}, watch, }; -use smol::{channel::Sender, Timer}; +use smol::channel::Sender; use std::{ cmp, collections::{HashMap, HashSet}, @@ -98,8 +98,24 @@ impl Worktree { scanner.run(event_stream) }); - ctx.spawn_stream(scan_state_rx, Self::observe_scan_state, |_, _| {}) - .detach(); + let handle = ctx.handle().downgrade(); + ctx.spawn(|mut ctx| async move { + while let Ok(scan_state) = scan_state_rx.recv().await { + let alive = ctx.update(|ctx| { + if let Some(handle) = handle.upgrade(&ctx) { + handle.update(ctx, |this, ctx| this.observe_scan_state(scan_state, ctx)); + true + } else { + false + } + }); + + if !alive { + break; + } + } + }) + .detach(); tree } @@ -116,15 +132,17 @@ impl Worktree { pub fn next_scan_complete(&self, ctx: &mut ModelContext) -> impl Future { let scan_id = self.snapshot.scan_id; - ctx.spawn_stream( - self.scan_state.1.clone(), - move |this, scan_state, ctx| { - if matches!(scan_state, ScanState::Idle) && this.snapshot.scan_id > scan_id { - ctx.halt_stream(); + let mut scan_state = self.scan_state.1.clone(); + let handle = ctx.handle(); + ctx.spawn(|ctx| async move { + while let Some(scan_state) = scan_state.recv().await { + if handle.read_with(&ctx, |this, _| { + matches!(scan_state, ScanState::Idle) && this.snapshot.scan_id > scan_id + }) { + break; } - }, - |_, _| {}, - ) + } + }) } fn observe_scan_state(&mut self, scan_state: ScanState, ctx: &mut ModelContext) { @@ -137,9 +155,12 @@ impl Worktree { ctx.notify(); if self.is_scanning() && !self.poll_scheduled { - ctx.spawn(Timer::after(Duration::from_millis(100)), |this, _, ctx| { - this.poll_scheduled = false; - this.poll_entries(ctx); + let handle = ctx.handle(); + ctx.spawn(|mut ctx| async move { + handle.update(&mut ctx, |this, ctx| { + this.poll_scheduled = false; + this.poll_entries(ctx); + }) }) .detach(); self.poll_scheduled = true; From a3be5595dda31484b49c7e3b588a39510bf7599c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 12 May 2021 15:28:59 -0600 Subject: [PATCH 6/7] Pass a handle to the current view model when spawning Most of the time, we'll want a way to get a reference back to the current view or model, so this facilitates that common case. --- gpui/src/app.rs | 10 +++++---- zed/src/editor/buffer/mod.rs | 3 +-- zed/src/editor/buffer_view.rs | 10 ++++----- zed/src/file_finder.rs | 5 ++--- zed/src/workspace.rs | 18 +++++++--------- zed/src/worktree.rs | 39 ++++++++++++++++++----------------- 6 files changed, 41 insertions(+), 44 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 3bcccef47c..4a99d712c7 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -1654,11 +1654,12 @@ impl<'a, T: Entity> ModelContext<'a, T> { pub fn spawn(&self, f: F) -> Task where - F: FnOnce(AsyncAppContext) -> Fut, + F: FnOnce(ModelHandle, AsyncAppContext) -> Fut, Fut: 'static + Future, S: 'static, { - self.app.spawn(f) + let handle = self.handle(); + self.app.spawn(|ctx| f(handle, ctx)) } } @@ -1909,11 +1910,12 @@ impl<'a, T: View> ViewContext<'a, T> { pub fn spawn(&self, f: F) -> Task where - F: FnOnce(AsyncAppContext) -> Fut, + F: FnOnce(ViewHandle, AsyncAppContext) -> Fut, Fut: 'static + Future, S: 'static, { - self.app.spawn(f) + let handle = self.handle(); + self.app.spawn(|ctx| f(handle, ctx)) } } diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 5d039fdc99..2ad1574498 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -477,9 +477,8 @@ impl Buffer { let snapshot = self.snapshot(); let version = self.version.clone(); let file = self.file.clone(); - let handle = ctx.handle(); - ctx.spawn(|mut ctx| async move { + ctx.spawn(|handle, mut ctx| async move { if let Some(file) = new_file.as_ref().or(file.as_ref()) { let result = ctx.read(|ctx| file.save(snapshot, ctx.as_ref())).await; if result.is_ok() { diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index cc22d8dcc8..df8d447a4a 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -2347,10 +2347,9 @@ impl BufferView { ctx.notify(); let epoch = self.next_blink_epoch(); - let handle = ctx.handle(); - ctx.spawn(|mut ctx| async move { + ctx.spawn(|this, mut ctx| async move { Timer::after(CURSOR_BLINK_INTERVAL).await; - handle.update(&mut ctx, |this, ctx| { + this.update(&mut ctx, |this, ctx| { this.resume_cursor_blinking(epoch, ctx); }) }) @@ -2370,10 +2369,9 @@ impl BufferView { ctx.notify(); let epoch = self.next_blink_epoch(); - let handle = ctx.handle(); - ctx.spawn(|mut ctx| async move { + ctx.spawn(|this, mut ctx| async move { Timer::after(CURSOR_BLINK_INTERVAL).await; - handle.update(&mut ctx, |this, ctx| this.blink_cursors(epoch, ctx)); + this.update(&mut ctx, |this, ctx| this.blink_cursors(epoch, ctx)); }) .detach(); } diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 0dc96e52fc..691bf4ae3f 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -415,10 +415,9 @@ impl FileFinder { (search_id, did_cancel, query, matches) }); - let handle = ctx.handle(); - ctx.spawn(|mut ctx| async move { + ctx.spawn(|this, mut ctx| async move { let matches = background_task.await; - handle.update(&mut ctx, |this, ctx| this.update_matches(matches, ctx)); + this.update(&mut ctx, |this, ctx| this.update_matches(matches, ctx)); }) .detach(); diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 81e5b345ae..87f52e486a 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -359,13 +359,12 @@ impl Workspace { .cloned() .zip(entries.into_iter()) .map(|(abs_path, file)| { - let handle = ctx.handle(); let is_file = bg.spawn(async move { abs_path.is_file() }); - ctx.spawn(|mut ctx| async move { + ctx.spawn(|this, mut ctx| async move { let is_file = is_file.await; - handle.update(&mut ctx, |me, ctx| { + this.update(&mut ctx, |this, ctx| { if is_file { - me.open_entry(file.entry_id(), ctx) + this.open_entry(file.entry_id(), ctx) } else { None } @@ -513,8 +512,7 @@ impl Workspace { let mut watch = self.loading_items.get(&entry).unwrap().clone(); - let handle = ctx.handle(); - Some(ctx.spawn(|mut ctx| async move { + Some(ctx.spawn(|this, mut ctx| async move { let load_result = loop { if let Some(load_result) = watch.borrow().as_ref() { break load_result.clone(); @@ -522,16 +520,16 @@ impl Workspace { watch.next().await; }; - handle.update(&mut ctx, |me, ctx| { - me.loading_items.remove(&entry); + this.update(&mut ctx, |this, ctx| { + this.loading_items.remove(&entry); match load_result { Ok(item) => { let weak_item = item.downgrade(); let view = weak_item .add_view(ctx.window_id(), settings, ctx.as_mut()) .unwrap(); - me.items.push(weak_item); - me.add_item_view(view, ctx); + this.items.push(weak_item); + this.add_item_view(view, ctx); } Err(error) => { log::error!("error opening item: {}", error); diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 55d8f91b99..80da089cf5 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -98,20 +98,23 @@ impl Worktree { scanner.run(event_stream) }); - let handle = ctx.handle().downgrade(); - ctx.spawn(|mut ctx| async move { - while let Ok(scan_state) = scan_state_rx.recv().await { - let alive = ctx.update(|ctx| { - if let Some(handle) = handle.upgrade(&ctx) { - handle.update(ctx, |this, ctx| this.observe_scan_state(scan_state, ctx)); - true - } else { - false - } - }); + ctx.spawn(|this, mut ctx| { + let this = this.downgrade(); + async move { + while let Ok(scan_state) = scan_state_rx.recv().await { + let alive = ctx.update(|ctx| { + if let Some(handle) = this.upgrade(&ctx) { + handle + .update(ctx, |this, ctx| this.observe_scan_state(scan_state, ctx)); + true + } else { + false + } + }); - if !alive { - break; + if !alive { + break; + } } } }) @@ -133,10 +136,9 @@ impl Worktree { pub fn next_scan_complete(&self, ctx: &mut ModelContext) -> impl Future { let scan_id = self.snapshot.scan_id; let mut scan_state = self.scan_state.1.clone(); - let handle = ctx.handle(); - ctx.spawn(|ctx| async move { + ctx.spawn(|this, ctx| async move { while let Some(scan_state) = scan_state.recv().await { - if handle.read_with(&ctx, |this, _| { + if this.read_with(&ctx, |this, _| { matches!(scan_state, ScanState::Idle) && this.snapshot.scan_id > scan_id }) { break; @@ -155,9 +157,8 @@ impl Worktree { ctx.notify(); if self.is_scanning() && !self.poll_scheduled { - let handle = ctx.handle(); - ctx.spawn(|mut ctx| async move { - handle.update(&mut ctx, |this, ctx| { + ctx.spawn(|this, mut ctx| async move { + this.update(&mut ctx, |this, ctx| { this.poll_scheduled = false; this.poll_entries(ctx); }) From d6c89521d9ce8f2292c0a2f5a730265b34507a4f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 12 May 2021 15:27:58 -0700 Subject: [PATCH 7/7] Remove commented-out tests for spawn and spawn_stream --- gpui/src/app.rs | 134 ------------------------------------------------ 1 file changed, 134 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 4a99d712c7..d539314bb7 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -2764,61 +2764,6 @@ mod tests { assert_eq!(handle_1.read(app).events, vec![7, 10, 5]) } - // #[crate::test(self)] - // async fn test_spawn_from_model(mut app: TestAppContext) { - // #[derive(Default)] - // struct Model { - // count: usize, - // } - - // impl Entity for Model { - // type Event = (); - // } - - // let handle = app.add_model(|_| Model::default()); - // handle - // .update(&mut app, |_, c| { - // c.spawn(async { 7 }, |model, output, _| { - // model.count = output; - // }) - // }) - // .await; - // app.read(|ctx| assert_eq!(handle.read(ctx).count, 7)); - - // handle - // .update(&mut app, |_, c| { - // c.spawn(async { 14 }, |model, output, _| { - // model.count = output; - // }) - // }) - // .await; - // app.read(|ctx| assert_eq!(handle.read(ctx).count, 14)); - // } - - // #[crate::test(self)] - // async fn test_spawn_stream_local_from_model(mut app: TestAppContext) { - // #[derive(Default)] - // struct Model { - // events: Vec>, - // } - - // impl Entity for Model { - // type Event = (); - // } - - // let handle = app.add_model(|_| Model::default()); - // handle - // .update(&mut app, |_, c| { - // c.spawn_stream( - // smol::stream::iter(vec![1, 2, 3]), - // |model, output, _| model.events.push(Some(output)), - // |model, _| model.events.push(None), - // ) - // }) - // .await; - // app.read(|ctx| assert_eq!(handle.read(ctx).events, [Some(1), Some(2), Some(3), None])); - // } - #[crate::test(self)] fn test_view_handles(app: &mut MutableAppContext) { struct View { @@ -3135,85 +3080,6 @@ mod tests { ); } - // #[crate::test(self)] - // async fn test_spawn_from_view(mut app: TestAppContext) { - // #[derive(Default)] - // struct View { - // count: usize, - // } - - // impl Entity for View { - // type Event = (); - // } - - // impl super::View for View { - // fn render<'a>(&self, _: &AppContext) -> ElementBox { - // Empty::new().boxed() - // } - - // fn ui_name() -> &'static str { - // "View" - // } - // } - - // let handle = app.add_window(|_| View::default()).1; - // handle - // .update(&mut app, |_, c| { - // c.spawn(async { 7 }, |me, output, _| { - // me.count = output; - // }) - // }) - // .await; - // app.read(|ctx| assert_eq!(handle.read(ctx).count, 7)); - // handle - // .update(&mut app, |_, c| { - // c.spawn(async { 14 }, |me, output, _| { - // me.count = output; - // }) - // }) - // .await; - // app.read(|ctx| assert_eq!(handle.read(ctx).count, 14)); - // } - - // #[crate::test(self)] - // async fn test_spawn_stream_local_from_view(mut app: TestAppContext) { - // #[derive(Default)] - // struct View { - // events: Vec>, - // } - - // impl Entity for View { - // type Event = (); - // } - - // impl super::View for View { - // fn render<'a>(&self, _: &AppContext) -> ElementBox { - // Empty::new().boxed() - // } - - // fn ui_name() -> &'static str { - // "View" - // } - // } - - // let (_, handle) = app.add_window(|_| View::default()); - // handle - // .update(&mut app, |_, c| { - // c.spawn_stream( - // smol::stream::iter(vec![1_usize, 2, 3]), - // |me, output, _| { - // me.events.push(Some(output)); - // }, - // |me, _| { - // me.events.push(None); - // }, - // ) - // }) - // .await; - - // app.read(|ctx| assert_eq!(handle.read(ctx).events, [Some(1), Some(2), Some(3), None])) - // } - #[crate::test(self)] fn test_dispatch_action(app: &mut MutableAppContext) { struct ViewA {