Uncomment flaky tests

Piotr Osiewicz created

Change summary

crates/editor2/src/display_map/wrap_map.rs | 665 +++++++++++-----------
crates/editor2/src/inlay_hint_cache.rs     | 681 ++++++++++++-----------
crates/gpui2/src/gpui2.rs                  |   2 
crates/gpui2/src/platform.rs               |   4 
crates/gpui2/src/scene.rs                  |   4 
crates/gpui2/src/test.rs                   |  31 +
6 files changed, 707 insertions(+), 680 deletions(-)

Detailed changes

crates/editor2/src/display_map/wrap_map.rs 🔗

@@ -1026,337 +1026,334 @@ fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
     }
 }
 
-// #[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;
-
-//     #[gpui::test(iterations = 100)]
-//     async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
-//         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);
-
-//         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;
-
-//         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::<String>();
-//                 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 (wrap_map, _) =
-//             cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, 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();
-//         }
-
-//         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 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());
-//                     });
-//                 }
-//             }
-
-//             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));
-
-//             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
-//                 );
-
-//                 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);
-//                         }
-//                     }
-
-//                     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::<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()));
-//     }
-
-//     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 wrap_text(
-//         unwrapped_text: &str,
-//         wrap_width: Option<f32>,
-//         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()
-//         }
-//     }
-
-//     impl WrapSnapshot {
-//         pub fn text(&self) -> String {
-//             self.text_chunks(0).collect()
-//         }
-
-//         pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
-//             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;
-
-//                 let mut expected_text = self.text_chunks(start_row).collect::<String>();
-//                 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::<Vec<_>>()
-//                     .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::<String>();
-//                 assert_eq!(
-//                     expected_text,
-//                     actual_text,
-//                     "chunks != highlighted_chunks for rows {:?}",
-//                     start_row..end_row
-//                 );
-//             }
-//         }
-//     }
-// }
+#[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) {
+        // 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);
+
+        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);
+
+        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::<String>();
+                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, 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, 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();
+        }
+
+        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 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(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());
+
+            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()) {
+                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,);
+                }
+
+                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,
+                    )
+                }
+            }
+        }
+
+        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::<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()));
+    }
+
+    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<Pixels>,
+        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()
+        }
+    }
+
+    impl WrapSnapshot {
+        pub fn text(&self) -> String {
+            self.text_chunks(0).collect()
+        }
+
+        pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
+            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;
+
+                let mut expected_text = self.text_chunks(start_row).collect::<String>();
+                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::<Vec<_>>()
+                    .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::<String>();
+                assert_eq!(
+                    expected_text,
+                    actual_text,
+                    "chunks != highlighted_chunks for rows {:?}",
+                    start_row..end_row
+                );
+            }
+        }
+    }
+}

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,
-    //             })
-    //         });
-
-    //         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::<Vec<_>>().join("")),
-    //                 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().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
-    //         });
-
-    //         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::<lsp::request::InlayHintRequest, _, _>(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,
-    //                     };
-
-    //                     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| {
-    //             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(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));
-
-    //             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(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::<Vec<_>>().join("")),
+                    "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().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
+        });
+
+        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::<lsp::request::InlayHintRequest, _, _>(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,
+                    };
+
+                    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| {
+            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(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));
+
+                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) {

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;

crates/gpui2/src/platform.rs 🔗

@@ -44,7 +44,7 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
     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<dyn PlatformTextSystem>;
@@ -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<Pixels>;
     fn scale_factor(&self) -> f32;

crates/gpui2/src/scene.rs 🔗

@@ -198,7 +198,7 @@ impl SceneBuilder {
     }
 }
 
-pub(crate) struct Scene {
+pub struct Scene {
     pub shadows: Vec<Shadow>,
     pub quads: Vec<Quad>,
     pub paths: Vec<Path<ScaledPixels>>,
@@ -214,7 +214,7 @@ impl Scene {
         &self.paths
     }
 
-    pub fn batches(&self) -> impl Iterator<Item = PrimitiveBatch> {
+    pub(crate) fn batches(&self) -> impl Iterator<Item = PrimitiveBatch> {
         BatchIterator {
             shadows: &self.shadows,
             shadows_start: 0,

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<T> {
+    rx: channel::Receiver<T>,
+    _subscription: Subscription,
+}
+
+impl<T: 'static> futures::Stream for Observation<T> {
+    type Item = T;
+
+    fn poll_next(
+        mut self: std::pin::Pin<&mut Self>,
+        cx: &mut std::task::Context<'_>,
+    ) -> std::task::Poll<Option<Self::Item>> {
+        self.rx.poll_next_unpin(cx)
+    }
+}
+
+pub fn observe<T: 'static>(entity: &impl Entity<T>, 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 }
+}