diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index c2325fa96d..817f7165ac 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -1026,337 +1026,334 @@ fn consolidate_wrap_edits(edits: &mut Vec) { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::{ -// display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, -// MultiBuffer, -// }; -// use gpui::test::observe; -// use rand::prelude::*; -// use settings::SettingsStore; -// use smol::stream::StreamExt; -// use std::{cmp, env, num::NonZeroU32}; -// use text::Rope; +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, + MultiBuffer, + }; + use gpui::{font, px, test::observe, Platform}; + use rand::prelude::*; + use settings::SettingsStore; + use smol::stream::StreamExt; + use std::{cmp, env, num::NonZeroU32}; + use text::Rope; + use theme::LoadThemes; -// #[gpui::test(iterations = 100)] -// async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { -// init_test(cx); + #[gpui::test(iterations = 100)] + async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { + // todo!() this test is flaky + init_test(cx); -// cx.background_executor.set_block_on_ticks(0..=50); -// let operations = env::var("OPERATIONS") -// .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) -// .unwrap_or(10); + cx.background_executor.set_block_on_ticks(0..=50); + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); -// let font_cache = cx.read(|cx| cx.font_cache().clone()); -// let font_system = cx.platform().fonts(); -// let mut wrap_width = if rng.gen_bool(0.1) { -// None -// } else { -// Some(rng.gen_range(0.0..=1000.0)) -// }; -// let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); -// let family_id = font_cache -// .load_family(&["Helvetica"], &Default::default()) -// .unwrap(); -// let font_id = font_cache -// .select_font(family_id, &Default::default()) -// .unwrap(); -// let font_size = 14.0; + let text_system = cx.test_platform.text_system(); + let mut wrap_width = if rng.gen_bool(0.1) { + None + } else { + Some(px(rng.gen_range(0.0..=1000.0))) + }; + let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); + let font = font("Helvetica"); + let font_id = text_system.font_id(&font).unwrap(); + let font_size = px(14.0); -// log::info!("Tab size: {}", tab_size); -// log::info!("Wrap width: {:?}", wrap_width); + log::info!("Tab size: {}", tab_size); + log::info!("Wrap width: {:?}", wrap_width); -// let buffer = cx.update(|cx| { -// if rng.gen() { -// MultiBuffer::build_random(&mut rng, cx) -// } else { -// let len = rng.gen_range(0..10); -// let text = util::RandomCharIter::new(&mut rng) -// .take(len) -// .collect::(); -// MultiBuffer::build_simple(&text, cx) -// } -// }); -// let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); -// log::info!("Buffer text: {:?}", buffer_snapshot.text()); -// let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); -// log::info!("InlayMap text: {:?}", inlay_snapshot.text()); -// let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone()); -// log::info!("FoldMap text: {:?}", fold_snapshot.text()); -// let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); -// let tabs_snapshot = tab_map.set_max_expansion_column(32); -// log::info!("TabMap text: {:?}", tabs_snapshot.text()); + let buffer = cx.update(|cx| { + if rng.gen() { + MultiBuffer::build_random(&mut rng, cx) + } else { + let len = rng.gen_range(0..10); + let text = util::RandomCharIter::new(&mut rng) + .take(len) + .collect::(); + MultiBuffer::build_simple(&text, cx) + } + }); + let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); + log::info!("Buffer text: {:?}", buffer_snapshot.text()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone()); + log::info!("FoldMap text: {:?}", fold_snapshot.text()); + let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); + let tabs_snapshot = tab_map.set_max_expansion_column(32); + log::info!("TabMap text: {:?}", tabs_snapshot.text()); -// let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system); -// let unwrapped_text = tabs_snapshot.text(); -// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); + let mut line_wrapper = LineWrapper::new(font_id, font_size, text_system); + let unwrapped_text = tabs_snapshot.text(); + let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); -// let (wrap_map, _) = -// cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)); -// let mut notifications = observe(&wrap_map, cx); + let (wrap_map, _) = + cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font, font_size, wrap_width, cx)); + let mut notifications = observe(&wrap_map, cx); -// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// notifications.next().await.unwrap(); -// } + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + notifications.next().await.unwrap(); + } -// let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| { -// assert!(!map.is_rewrapping()); -// map.sync(tabs_snapshot.clone(), Vec::new(), cx) -// }); + let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| { + assert!(!map.is_rewrapping()); + map.sync(tabs_snapshot.clone(), Vec::new(), cx) + }); -// let actual_text = initial_snapshot.text(); -// assert_eq!( -// actual_text, expected_text, -// "unwrapped text is: {:?}", -// unwrapped_text -// ); -// log::info!("Wrapped text: {:?}", actual_text); + let actual_text = initial_snapshot.text(); + assert_eq!( + actual_text, expected_text, + "unwrapped text is: {:?}", + unwrapped_text + ); + log::info!("Wrapped text: {:?}", actual_text); -// let mut next_inlay_id = 0; -// let mut edits = Vec::new(); -// for _i in 0..operations { -// log::info!("{} ==============================================", _i); + let mut next_inlay_id = 0; + let mut edits = Vec::new(); + for _i in 0..operations { + log::info!("{} ==============================================", _i); -// let mut buffer_edits = Vec::new(); -// match rng.gen_range(0..=100) { -// 0..=19 => { -// wrap_width = if rng.gen_bool(0.2) { -// None -// } else { -// Some(rng.gen_range(0.0..=1000.0)) -// }; -// log::info!("Setting wrap width to {:?}", wrap_width); -// wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); -// } -// 20..=39 => { -// for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { -// let (tabs_snapshot, tab_edits) = -// tab_map.sync(fold_snapshot, fold_edits, tab_size); -// let (mut snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); -// snapshot.check_invariants(); -// snapshot.verify_chunks(&mut rng); -// edits.push((snapshot, wrap_edits)); -// } -// } -// 40..=59 => { -// let (inlay_snapshot, inlay_edits) = -// inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); -// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); -// let (tabs_snapshot, tab_edits) = -// tab_map.sync(fold_snapshot, fold_edits, tab_size); -// let (mut snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); -// snapshot.check_invariants(); -// snapshot.verify_chunks(&mut rng); -// edits.push((snapshot, wrap_edits)); -// } -// _ => { -// buffer.update(cx, |buffer, cx| { -// let subscription = buffer.subscribe(); -// let edit_count = rng.gen_range(1..=5); -// buffer.randomly_mutate(&mut rng, edit_count, cx); -// buffer_snapshot = buffer.snapshot(cx); -// buffer_edits.extend(subscription.consume()); -// }); -// } -// } + let mut buffer_edits = Vec::new(); + match rng.gen_range(0..=100) { + 0..=19 => { + wrap_width = if rng.gen_bool(0.2) { + None + } else { + Some(px(rng.gen_range(0.0..=1000.0))) + }; + log::info!("Setting wrap width to {:?}", wrap_width); + wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); + } + 20..=39 => { + for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { + let (tabs_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (mut snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); + } + } + 40..=59 => { + let (inlay_snapshot, inlay_edits) = + inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + let (tabs_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (mut snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); + } + _ => { + buffer.update(cx, |buffer, cx| { + let subscription = buffer.subscribe(); + let edit_count = rng.gen_range(1..=5); + buffer.randomly_mutate(&mut rng, edit_count, cx); + buffer_snapshot = buffer.snapshot(cx); + buffer_edits.extend(subscription.consume()); + }); + } + } -// log::info!("Buffer text: {:?}", buffer_snapshot.text()); -// let (inlay_snapshot, inlay_edits) = -// inlay_map.sync(buffer_snapshot.clone(), buffer_edits); -// log::info!("InlayMap text: {:?}", inlay_snapshot.text()); -// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); -// log::info!("FoldMap text: {:?}", fold_snapshot.text()); -// let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); -// log::info!("TabMap text: {:?}", tabs_snapshot.text()); + log::info!("Buffer text: {:?}", buffer_snapshot.text()); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), buffer_edits); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + log::info!("FoldMap text: {:?}", fold_snapshot.text()); + let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); + log::info!("TabMap text: {:?}", tabs_snapshot.text()); -// let unwrapped_text = tabs_snapshot.text(); -// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); -// let (mut snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx)); -// snapshot.check_invariants(); -// snapshot.verify_chunks(&mut rng); -// edits.push((snapshot, wrap_edits)); + let unwrapped_text = tabs_snapshot.text(); + let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); + let (mut snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx)); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); -// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { -// log::info!("Waiting for wrapping to finish"); -// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// notifications.next().await.unwrap(); -// } -// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); -// } + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { + log::info!("Waiting for wrapping to finish"); + while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + notifications.next().await.unwrap(); + } + wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); + } -// if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// let (mut wrapped_snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); -// let actual_text = wrapped_snapshot.text(); -// let actual_longest_row = wrapped_snapshot.longest_row(); -// log::info!("Wrapping finished: {:?}", actual_text); -// wrapped_snapshot.check_invariants(); -// wrapped_snapshot.verify_chunks(&mut rng); -// edits.push((wrapped_snapshot.clone(), wrap_edits)); -// assert_eq!( -// actual_text, expected_text, -// "unwrapped text is: {:?}", -// unwrapped_text -// ); + if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + let (mut wrapped_snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); + let actual_text = wrapped_snapshot.text(); + let actual_longest_row = wrapped_snapshot.longest_row(); + log::info!("Wrapping finished: {:?}", actual_text); + wrapped_snapshot.check_invariants(); + wrapped_snapshot.verify_chunks(&mut rng); + edits.push((wrapped_snapshot.clone(), wrap_edits)); + assert_eq!( + actual_text, expected_text, + "unwrapped text is: {:?}", + unwrapped_text + ); -// let mut summary = TextSummary::default(); -// for (ix, item) in wrapped_snapshot -// .transforms -// .items(&()) -// .into_iter() -// .enumerate() -// { -// summary += &item.summary.output; -// log::info!("{} summary: {:?}", ix, item.summary.output,); -// } + let mut summary = TextSummary::default(); + for (ix, item) in wrapped_snapshot + .transforms + .items(&()) + .into_iter() + .enumerate() + { + summary += &item.summary.output; + log::info!("{} summary: {:?}", ix, item.summary.output,); + } -// if tab_size.get() == 1 -// || !wrapped_snapshot -// .tab_snapshot -// .fold_snapshot -// .text() -// .contains('\t') -// { -// let mut expected_longest_rows = Vec::new(); -// let mut longest_line_len = -1; -// for (row, line) in expected_text.split('\n').enumerate() { -// let line_char_count = line.chars().count() as isize; -// if line_char_count > longest_line_len { -// expected_longest_rows.clear(); -// longest_line_len = line_char_count; -// } -// if line_char_count >= longest_line_len { -// expected_longest_rows.push(row as u32); -// } -// } + if tab_size.get() == 1 + || !wrapped_snapshot + .tab_snapshot + .fold_snapshot + .text() + .contains('\t') + { + let mut expected_longest_rows = Vec::new(); + let mut longest_line_len = -1; + for (row, line) in expected_text.split('\n').enumerate() { + let line_char_count = line.chars().count() as isize; + if line_char_count > longest_line_len { + expected_longest_rows.clear(); + longest_line_len = line_char_count; + } + if line_char_count >= longest_line_len { + expected_longest_rows.push(row as u32); + } + } -// assert!( -// expected_longest_rows.contains(&actual_longest_row), -// "incorrect longest row {}. expected {:?} with length {}", -// actual_longest_row, -// expected_longest_rows, -// longest_line_len, -// ) -// } -// } -// } + assert!( + expected_longest_rows.contains(&actual_longest_row), + "incorrect longest row {}. expected {:?} with length {}", + actual_longest_row, + expected_longest_rows, + longest_line_len, + ) + } + } + } -// let mut initial_text = Rope::from(initial_snapshot.text().as_str()); -// for (snapshot, patch) in edits { -// let snapshot_text = Rope::from(snapshot.text().as_str()); -// for edit in &patch { -// let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0)); -// let old_end = initial_text.point_to_offset(cmp::min( -// Point::new(edit.new.start + edit.old.len() as u32, 0), -// initial_text.max_point(), -// )); -// let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0)); -// let new_end = snapshot_text.point_to_offset(cmp::min( -// Point::new(edit.new.end, 0), -// snapshot_text.max_point(), -// )); -// let new_text = snapshot_text -// .chunks_in_range(new_start..new_end) -// .collect::(); + let mut initial_text = Rope::from(initial_snapshot.text().as_str()); + for (snapshot, patch) in edits { + let snapshot_text = Rope::from(snapshot.text().as_str()); + for edit in &patch { + let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0)); + let old_end = initial_text.point_to_offset(cmp::min( + Point::new(edit.new.start + edit.old.len() as u32, 0), + initial_text.max_point(), + )); + let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0)); + let new_end = snapshot_text.point_to_offset(cmp::min( + Point::new(edit.new.end, 0), + snapshot_text.max_point(), + )); + let new_text = snapshot_text + .chunks_in_range(new_start..new_end) + .collect::(); -// initial_text.replace(old_start..old_end, &new_text); -// } -// assert_eq!(initial_text.to_string(), snapshot_text.to_string()); -// } + initial_text.replace(old_start..old_end, &new_text); + } + assert_eq!(initial_text.to_string(), snapshot_text.to_string()); + } -// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// log::info!("Waiting for wrapping to finish"); -// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// notifications.next().await.unwrap(); -// } -// } -// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); -// } + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + log::info!("Waiting for wrapping to finish"); + while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + notifications.next().await.unwrap(); + } + } + wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); + } -// fn init_test(cx: &mut gpui::TestAppContext) { -// cx.foreground_executor().forbid_parking(); -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// theme::init((), cx); -// }); -// } + fn init_test(cx: &mut gpui::TestAppContext) { + cx.update(|cx| { + let settings = SettingsStore::test(cx); + cx.set_global(settings); + theme::init(LoadThemes::JustBase, cx); + }); + } -// fn wrap_text( -// unwrapped_text: &str, -// wrap_width: Option, -// line_wrapper: &mut LineWrapper, -// ) -> String { -// if let Some(wrap_width) = wrap_width { -// let mut wrapped_text = String::new(); -// for (row, line) in unwrapped_text.split('\n').enumerate() { -// if row > 0 { -// wrapped_text.push('\n') -// } + fn wrap_text( + unwrapped_text: &str, + wrap_width: Option, + line_wrapper: &mut LineWrapper, + ) -> String { + if let Some(wrap_width) = wrap_width { + let mut wrapped_text = String::new(); + for (row, line) in unwrapped_text.split('\n').enumerate() { + if row > 0 { + wrapped_text.push('\n') + } -// let mut prev_ix = 0; -// for boundary in line_wrapper.wrap_line(line, wrap_width) { -// wrapped_text.push_str(&line[prev_ix..boundary.ix]); -// wrapped_text.push('\n'); -// wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); -// prev_ix = boundary.ix; -// } -// wrapped_text.push_str(&line[prev_ix..]); -// } -// wrapped_text -// } else { -// unwrapped_text.to_string() -// } -// } + let mut prev_ix = 0; + for boundary in line_wrapper.wrap_line(line, wrap_width) { + wrapped_text.push_str(&line[prev_ix..boundary.ix]); + wrapped_text.push('\n'); + wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); + prev_ix = boundary.ix; + } + wrapped_text.push_str(&line[prev_ix..]); + } + wrapped_text + } else { + unwrapped_text.to_string() + } + } -// impl WrapSnapshot { -// pub fn text(&self) -> String { -// self.text_chunks(0).collect() -// } + impl WrapSnapshot { + pub fn text(&self) -> String { + self.text_chunks(0).collect() + } -// pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { -// self.chunks( -// wrap_row..self.max_point().row() + 1, -// false, -// Highlights::default(), -// ) -// .map(|h| h.text) -// } + pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { + self.chunks( + wrap_row..self.max_point().row() + 1, + false, + Highlights::default(), + ) + .map(|h| h.text) + } -// fn verify_chunks(&mut self, rng: &mut impl Rng) { -// for _ in 0..5 { -// let mut end_row = rng.gen_range(0..=self.max_point().row()); -// let start_row = rng.gen_range(0..=end_row); -// end_row += 1; + fn verify_chunks(&mut self, rng: &mut impl Rng) { + for _ in 0..5 { + let mut end_row = rng.gen_range(0..=self.max_point().row()); + let start_row = rng.gen_range(0..=end_row); + end_row += 1; -// let mut expected_text = self.text_chunks(start_row).collect::(); -// if expected_text.ends_with('\n') { -// expected_text.push('\n'); -// } -// let mut expected_text = expected_text -// .lines() -// .take((end_row - start_row) as usize) -// .collect::>() -// .join("\n"); -// if end_row <= self.max_point().row() { -// expected_text.push('\n'); -// } + let mut expected_text = self.text_chunks(start_row).collect::(); + if expected_text.ends_with('\n') { + expected_text.push('\n'); + } + let mut expected_text = expected_text + .lines() + .take((end_row - start_row) as usize) + .collect::>() + .join("\n"); + if end_row <= self.max_point().row() { + expected_text.push('\n'); + } -// let actual_text = self -// .chunks(start_row..end_row, true, Highlights::default()) -// .map(|c| c.text) -// .collect::(); -// assert_eq!( -// expected_text, -// actual_text, -// "chunks != highlighted_chunks for rows {:?}", -// start_row..end_row -// ); -// } -// } -// } -// } + let actual_text = self + .chunks(start_row..end_row, true, Highlights::default()) + .map(|c| c.text) + .collect::(); + assert_eq!( + expected_text, + actual_text, + "chunks != highlighted_chunks for rows {:?}", + start_row..end_row + ); + } + } + } +} diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index 36b4e6af66..c3722e214c 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -2401,346 +2401,347 @@ pub mod tests { }); } - // #[gpui::test(iterations = 10)] - // async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { - // init_test(cx, |settings| { - // settings.defaults.inlay_hints = Some(InlayHintSettings { - // enabled: true, - // show_type_hints: true, - // show_parameter_hints: true, - // show_other_hints: true, - // }) - // }); + #[gpui::test(iterations = 10)] + async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { + // todo!() this test is flaky + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); - // let mut language = Language::new( - // LanguageConfig { - // name: "Rust".into(), - // path_suffixes: vec!["rs".to_string()], - // ..Default::default() - // }, - // Some(tree_sitter_rust::language()), - // ); - // let mut fake_servers = language - // .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - // capabilities: lsp::ServerCapabilities { - // inlay_hint_provider: Some(lsp::OneOf::Left(true)), - // ..Default::default() - // }, - // ..Default::default() - // })) - // .await; - // let language = Arc::new(language); - // let fs = FakeFs::new(cx.background_executor.clone()); - // fs.insert_tree( - // "/a", - // json!({ - // "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), - // "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), - // }), - // ) - // .await; - // let project = Project::test(fs, ["/a".as_ref()], cx).await; - // project.update(cx, |project, _| { - // project.languages().add(Arc::clone(&language)) - // }); - // let worktree_id = project.update(cx, |project, cx| { - // project.worktrees().next().unwrap().read(cx).id() - // }); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), + "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::clone(&language)) + }); + let worktree_id = project.update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }); - // let buffer_1 = project - // .update(cx, |project, cx| { - // project.open_buffer((worktree_id, "main.rs"), cx) - // }) - // .await - // .unwrap(); - // let buffer_2 = project - // .update(cx, |project, cx| { - // project.open_buffer((worktree_id, "other.rs"), cx) - // }) - // .await - // .unwrap(); - // let multibuffer = cx.build_model(|cx| { - // let mut multibuffer = MultiBuffer::new(0); - // multibuffer.push_excerpts( - // buffer_1.clone(), - // [ - // ExcerptRange { - // context: Point::new(0, 0)..Point::new(2, 0), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(4, 0)..Point::new(11, 0), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(22, 0)..Point::new(33, 0), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(44, 0)..Point::new(55, 0), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(56, 0)..Point::new(66, 0), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(67, 0)..Point::new(77, 0), - // primary: None, - // }, - // ], - // cx, - // ); - // multibuffer.push_excerpts( - // buffer_2.clone(), - // [ - // ExcerptRange { - // context: Point::new(0, 1)..Point::new(2, 1), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(4, 1)..Point::new(11, 1), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(22, 1)..Point::new(33, 1), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(44, 1)..Point::new(55, 1), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(56, 1)..Point::new(66, 1), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(67, 1)..Point::new(77, 1), - // primary: None, - // }, - // ], - // cx, - // ); - // multibuffer - // }); + let buffer_1 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "other.rs"), cx) + }) + .await + .unwrap(); + let multibuffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer_1.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(4, 0)..Point::new(11, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(22, 0)..Point::new(33, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(44, 0)..Point::new(55, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(56, 0)..Point::new(66, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(67, 0)..Point::new(77, 0), + primary: None, + }, + ], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ + ExcerptRange { + context: Point::new(0, 1)..Point::new(2, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(4, 1)..Point::new(11, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(22, 1)..Point::new(33, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(44, 1)..Point::new(55, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(56, 1)..Point::new(66, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(67, 1)..Point::new(77, 1), + primary: None, + }, + ], + cx, + ); + multibuffer + }); - // cx.executor().run_until_parked(); - // let editor = - // cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); - // let editor_edited = Arc::new(AtomicBool::new(false)); - // let fake_server = fake_servers.next().await.unwrap(); - // let closure_editor_edited = Arc::clone(&editor_edited); - // fake_server - // .handle_request::(move |params, _| { - // let task_editor_edited = Arc::clone(&closure_editor_edited); - // async move { - // let hint_text = if params.text_document.uri - // == lsp::Url::from_file_path("/a/main.rs").unwrap() - // { - // "main hint" - // } else if params.text_document.uri - // == lsp::Url::from_file_path("/a/other.rs").unwrap() - // { - // "other hint" - // } else { - // panic!("unexpected uri: {:?}", params.text_document.uri); - // }; + cx.executor().run_until_parked(); + let editor = + cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + let editor_edited = Arc::new(AtomicBool::new(false)); + let fake_server = fake_servers.next().await.unwrap(); + let closure_editor_edited = Arc::clone(&editor_edited); + fake_server + .handle_request::(move |params, _| { + let task_editor_edited = Arc::clone(&closure_editor_edited); + async move { + let hint_text = if params.text_document.uri + == lsp::Url::from_file_path("/a/main.rs").unwrap() + { + "main hint" + } else if params.text_document.uri + == lsp::Url::from_file_path("/a/other.rs").unwrap() + { + "other hint" + } else { + panic!("unexpected uri: {:?}", params.text_document.uri); + }; - // // one hint per excerpt - // let positions = [ - // lsp::Position::new(0, 2), - // lsp::Position::new(4, 2), - // lsp::Position::new(22, 2), - // lsp::Position::new(44, 2), - // lsp::Position::new(56, 2), - // lsp::Position::new(67, 2), - // ]; - // let out_of_range_hint = lsp::InlayHint { - // position: lsp::Position::new( - // params.range.start.line + 99, - // params.range.start.character + 99, - // ), - // label: lsp::InlayHintLabel::String( - // "out of excerpt range, should be ignored".to_string(), - // ), - // kind: None, - // text_edits: None, - // tooltip: None, - // padding_left: None, - // padding_right: None, - // data: None, - // }; + // one hint per excerpt + let positions = [ + lsp::Position::new(0, 2), + lsp::Position::new(4, 2), + lsp::Position::new(22, 2), + lsp::Position::new(44, 2), + lsp::Position::new(56, 2), + lsp::Position::new(67, 2), + ]; + let out_of_range_hint = lsp::InlayHint { + position: lsp::Position::new( + params.range.start.line + 99, + params.range.start.character + 99, + ), + label: lsp::InlayHintLabel::String( + "out of excerpt range, should be ignored".to_string(), + ), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }; - // let edited = task_editor_edited.load(Ordering::Acquire); - // Ok(Some( - // std::iter::once(out_of_range_hint) - // .chain(positions.into_iter().enumerate().map(|(i, position)| { - // lsp::InlayHint { - // position, - // label: lsp::InlayHintLabel::String(format!( - // "{hint_text}{} #{i}", - // if edited { "(edited)" } else { "" }, - // )), - // kind: None, - // text_edits: None, - // tooltip: None, - // padding_left: None, - // padding_right: None, - // data: None, - // } - // })) - // .collect(), - // )) - // } - // }) - // .next() - // .await; - // cx.executor().run_until_parked(); + let edited = task_editor_edited.load(Ordering::Acquire); + Ok(Some( + std::iter::once(out_of_range_hint) + .chain(positions.into_iter().enumerate().map(|(i, position)| { + lsp::InlayHint { + position, + label: lsp::InlayHintLabel::String(format!( + "{hint_text}{} #{i}", + if edited { "(edited)" } else { "" }, + )), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + } + })) + .collect(), + )) + } + }) + .next() + .await; + cx.executor().run_until_parked(); - // editor.update(cx, |editor, cx| { - // let expected_hints = vec![ - // "main hint #0".to_string(), - // "main hint #1".to_string(), - // "main hint #2".to_string(), - // "main hint #3".to_string(), - // // todo!() there used to be no these hints, but new gpui2 presumably scrolls a bit farther - // // (or renders less?) note that tests below pass - // "main hint #4".to_string(), - // "main hint #5".to_string(), - // ]; - // assert_eq!( - // expected_hints, - // cached_hint_labels(editor), - // "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" - // ); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); - // }); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + // todo!() there used to be no these hints, but new gpui2 presumably scrolls a bit farther + // (or renders less?) note that tests below pass + "main hint #4".to_string(), + "main hint #5".to_string(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); + }); - // editor.update(cx, |editor, cx| { - // editor.change_selections(Some(Autoscroll::Next), cx, |s| { - // s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) - // }); - // editor.change_selections(Some(Autoscroll::Next), cx, |s| { - // s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) - // }); - // editor.change_selections(Some(Autoscroll::Next), cx, |s| { - // s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) - // }); - // }); - // cx.executor().run_until_parked(); - // editor.update(cx, |editor, cx| { - // let expected_hints = vec![ - // "main hint #0".to_string(), - // "main hint #1".to_string(), - // "main hint #2".to_string(), - // "main hint #3".to_string(), - // "main hint #4".to_string(), - // "main hint #5".to_string(), - // "other hint #0".to_string(), - // "other hint #1".to_string(), - // "other hint #2".to_string(), - // ]; - // assert_eq!(expected_hints, cached_hint_labels(editor), - // "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), - // "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); - // }); + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + }); + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) + }); + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) + }); + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), + "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); + }); - // editor.update(cx, |editor, cx| { - // editor.change_selections(Some(Autoscroll::Next), cx, |s| { - // s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) - // }); - // }); - // cx.executor().advance_clock(Duration::from_millis( - // INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - // )); - // cx.executor().run_until_parked(); - // let last_scroll_update_version = editor.update(cx, |editor, cx| { - // let expected_hints = vec![ - // "main hint #0".to_string(), - // "main hint #1".to_string(), - // "main hint #2".to_string(), - // "main hint #3".to_string(), - // "main hint #4".to_string(), - // "main hint #5".to_string(), - // "other hint #0".to_string(), - // "other hint #1".to_string(), - // "other hint #2".to_string(), - // "other hint #3".to_string(), - // "other hint #4".to_string(), - // "other hint #5".to_string(), - // ]; - // assert_eq!(expected_hints, cached_hint_labels(editor), - // "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); - // expected_hints.len() - // }).unwrap(); + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) + }); + }); + cx.executor().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.executor().run_until_parked(); + let last_scroll_update_version = editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + "other hint #4".to_string(), + "other hint #5".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); + expected_hints.len() + }).unwrap(); - // editor.update(cx, |editor, cx| { - // editor.change_selections(Some(Autoscroll::Next), cx, |s| { - // s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) - // }); - // }); - // cx.executor().run_until_parked(); - // editor.update(cx, |editor, cx| { - // let expected_hints = vec![ - // "main hint #0".to_string(), - // "main hint #1".to_string(), - // "main hint #2".to_string(), - // "main hint #3".to_string(), - // "main hint #4".to_string(), - // "main hint #5".to_string(), - // "other hint #0".to_string(), - // "other hint #1".to_string(), - // "other hint #2".to_string(), - // "other hint #3".to_string(), - // "other hint #4".to_string(), - // "other hint #5".to_string(), - // ]; - // assert_eq!(expected_hints, cached_hint_labels(editor), - // "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); - // }); + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + }); + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + "other hint #4".to_string(), + "other hint #5".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); + }); - // editor_edited.store(true, Ordering::Release); - // editor.update(cx, |editor, cx| { - // editor.change_selections(None, cx, |s| { - // s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) - // }); - // editor.handle_input("++++more text++++", cx); - // }); - // cx.executor().run_until_parked(); - // editor.update(cx, |editor, cx| { - // let expected_hints = vec![ - // "main hint(edited) #0".to_string(), - // "main hint(edited) #1".to_string(), - // "main hint(edited) #2".to_string(), - // "main hint(edited) #3".to_string(), - // "main hint(edited) #4".to_string(), - // "main hint(edited) #5".to_string(), - // "other hint(edited) #0".to_string(), - // "other hint(edited) #1".to_string(), - // ]; - // assert_eq!( - // expected_hints, - // cached_hint_labels(editor), - // "After multibuffer edit, editor gets scolled back to the last selection; \ - // all hints should be invalidated and requeried for all of its visible excerpts" - // ); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + editor_edited.store(true, Ordering::Release); + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + }); + editor.handle_input("++++more text++++", cx); + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint(edited) #0".to_string(), + "main hint(edited) #1".to_string(), + "main hint(edited) #2".to_string(), + "main hint(edited) #3".to_string(), + "main hint(edited) #4".to_string(), + "main hint(edited) #5".to_string(), + "other hint(edited) #0".to_string(), + "other hint(edited) #1".to_string(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "After multibuffer edit, editor gets scolled back to the last selection; \ + all hints should be invalidated and requeried for all of its visible excerpts" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // let current_cache_version = editor.inlay_hint_cache().version; - // let minimum_expected_version = last_scroll_update_version + expected_hints.len(); - // assert!( - // current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, - // "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" - // ); - // }); - // } + let current_cache_version = editor.inlay_hint_cache().version; + let minimum_expected_version = last_scroll_update_version + expected_hints.len(); + assert!( + current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, + "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" + ); + }); + } #[gpui::test] async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) { diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 984859f1b0..5b88286240 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -21,7 +21,7 @@ mod subscription; mod svg_renderer; mod taffy; #[cfg(any(test, feature = "test-support"))] -mod test; +pub mod test; mod text_system; mod util; mod view; diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 7375f47939..37b156e348 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -44,7 +44,7 @@ pub(crate) fn current_platform() -> Rc { Rc::new(MacPlatform::new()) } -pub(crate) trait Platform: 'static { +pub trait Platform: 'static { fn background_executor(&self) -> BackgroundExecutor; fn foreground_executor(&self) -> ForegroundExecutor; fn text_system(&self) -> Arc; @@ -128,7 +128,7 @@ impl Debug for DisplayId { unsafe impl Send for DisplayId {} -pub(crate) trait PlatformWindow { +pub trait PlatformWindow { fn bounds(&self) -> WindowBounds; fn content_size(&self) -> Size; fn scale_factor(&self) -> f32; diff --git a/crates/gpui2/src/scene.rs b/crates/gpui2/src/scene.rs index 5492605602..ca0a50546e 100644 --- a/crates/gpui2/src/scene.rs +++ b/crates/gpui2/src/scene.rs @@ -198,7 +198,7 @@ impl SceneBuilder { } } -pub(crate) struct Scene { +pub struct Scene { pub shadows: Vec, pub quads: Vec, pub paths: Vec>, @@ -214,7 +214,7 @@ impl Scene { &self.paths } - pub fn batches(&self) -> impl Iterator { + pub(crate) fn batches(&self) -> impl Iterator { BatchIterator { shadows: &self.shadows, shadows_start: 0, diff --git a/crates/gpui2/src/test.rs b/crates/gpui2/src/test.rs index 3f2697f7e3..5a21576fb2 100644 --- a/crates/gpui2/src/test.rs +++ b/crates/gpui2/src/test.rs @@ -1,5 +1,7 @@ -use crate::TestDispatcher; +use crate::{Entity, Subscription, TestAppContext, TestDispatcher}; +use futures::StreamExt as _; use rand::prelude::*; +use smol::channel; use std::{ env, panic::{self, RefUnwindSafe}, @@ -49,3 +51,30 @@ pub fn run_test( } } } + +pub struct Observation { + rx: channel::Receiver, + _subscription: Subscription, +} + +impl futures::Stream for Observation { + type Item = T; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.rx.poll_next_unpin(cx) + } +} + +pub fn observe(entity: &impl Entity, cx: &mut TestAppContext) -> Observation<()> { + let (tx, rx) = smol::channel::unbounded(); + let _subscription = cx.update(|cx| { + cx.observe(entity, move |_, _| { + let _ = smol::block_on(tx.send(())); + }) + }); + + Observation { rx, _subscription } +}