Get editor tests compiling

Mikayla created

Change summary

crates/editor2/src/editor.rs                       |  124 
crates/editor2/src/editor_tests.rs                 | 1177 ++++++++-------
crates/editor2/src/test/editor_lsp_test_context.rs |   18 
crates/editor2/src/test/editor_test_context.rs     |  129 +
crates/gpui2/src/app/test_context.rs               |   82 +
crates/gpui2/src/color.rs                          |   31 
crates/gpui2/src/util.rs                           |   26 
crates/workspace2/src/workspace2.rs                |   34 
8 files changed, 902 insertions(+), 719 deletions(-)

Detailed changes

crates/editor2/src/editor.rs 🔗

@@ -10056,76 +10056,76 @@ pub fn diagnostic_style(
     }
 }
 
-// pub fn combine_syntax_and_fuzzy_match_highlights(
-//     text: &str,
-//     default_style: HighlightStyle,
-//     syntax_ranges: impl Iterator<Item = (Range<usize>, HighlightStyle)>,
-//     match_indices: &[usize],
-// ) -> Vec<(Range<usize>, HighlightStyle)> {
-//     let mut result = Vec::new();
-//     let mut match_indices = match_indices.iter().copied().peekable();
-
-//     for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())])
-//     {
-//         syntax_highlight.weight = None;
-
-//         // Add highlights for any fuzzy match characters before the next
-//         // syntax highlight range.
-//         while let Some(&match_index) = match_indices.peek() {
-//             if match_index >= range.start {
-//                 break;
-//             }
-//             match_indices.next();
-//             let end_index = char_ix_after(match_index, text);
-//             let mut match_style = default_style;
-//             match_style.weight = Some(FontWeight::BOLD);
-//             result.push((match_index..end_index, match_style));
-//         }
+pub fn combine_syntax_and_fuzzy_match_highlights(
+    text: &str,
+    default_style: HighlightStyle,
+    syntax_ranges: impl Iterator<Item = (Range<usize>, HighlightStyle)>,
+    match_indices: &[usize],
+) -> Vec<(Range<usize>, HighlightStyle)> {
+    let mut result = Vec::new();
+    let mut match_indices = match_indices.iter().copied().peekable();
+
+    for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())])
+    {
+        syntax_highlight.font_weight = None;
 
-//         if range.start == usize::MAX {
-//             break;
-//         }
+        // Add highlights for any fuzzy match characters before the next
+        // syntax highlight range.
+        while let Some(&match_index) = match_indices.peek() {
+            if match_index >= range.start {
+                break;
+            }
+            match_indices.next();
+            let end_index = char_ix_after(match_index, text);
+            let mut match_style = default_style;
+            match_style.font_weight = Some(FontWeight::BOLD);
+            result.push((match_index..end_index, match_style));
+        }
 
-//         // Add highlights for any fuzzy match characters within the
-//         // syntax highlight range.
-//         let mut offset = range.start;
-//         while let Some(&match_index) = match_indices.peek() {
-//             if match_index >= range.end {
-//                 break;
-//             }
+        if range.start == usize::MAX {
+            break;
+        }
 
-//             match_indices.next();
-//             if match_index > offset {
-//                 result.push((offset..match_index, syntax_highlight));
-//             }
+        // Add highlights for any fuzzy match characters within the
+        // syntax highlight range.
+        let mut offset = range.start;
+        while let Some(&match_index) = match_indices.peek() {
+            if match_index >= range.end {
+                break;
+            }
 
-//             let mut end_index = char_ix_after(match_index, text);
-//             while let Some(&next_match_index) = match_indices.peek() {
-//                 if next_match_index == end_index && next_match_index < range.end {
-//                     end_index = char_ix_after(next_match_index, text);
-//                     match_indices.next();
-//                 } else {
-//                     break;
-//                 }
-//             }
+            match_indices.next();
+            if match_index > offset {
+                result.push((offset..match_index, syntax_highlight));
+            }
 
-//             let mut match_style = syntax_highlight;
-//             match_style.weight = Some(FontWeight::BOLD);
-//             result.push((match_index..end_index, match_style));
-//             offset = end_index;
-//         }
+            let mut end_index = char_ix_after(match_index, text);
+            while let Some(&next_match_index) = match_indices.peek() {
+                if next_match_index == end_index && next_match_index < range.end {
+                    end_index = char_ix_after(next_match_index, text);
+                    match_indices.next();
+                } else {
+                    break;
+                }
+            }
 
-//         if offset < range.end {
-//             result.push((offset..range.end, syntax_highlight));
-//         }
-//     }
+            let mut match_style = syntax_highlight;
+            match_style.font_weight = Some(FontWeight::BOLD);
+            result.push((match_index..end_index, match_style));
+            offset = end_index;
+        }
 
-//     fn char_ix_after(ix: usize, text: &str) -> usize {
-//         ix + text[ix..].chars().next().unwrap().len_utf8()
-//     }
+        if offset < range.end {
+            result.push((offset..range.end, syntax_highlight));
+        }
+    }
 
-//     result
-// }
+    fn char_ix_after(ix: usize, text: &str) -> usize {
+        ix + text[ix..].chars().next().unwrap().len_utf8()
+    }
+
+    result
+}
 
 // pub fn styled_runs_for_code_label<'a>(
 //     label: &'a CodeLabel,

crates/editor2/src/editor_tests.rs 🔗

@@ -50,7 +50,8 @@ fn test_edit_events(cx: &mut TestAppContext) {
     let editor1 = cx.add_window({
         let events = events.clone();
         |cx| {
-            cx.subscribe(cx.view(), move |_, _, event, _| {
+            let view = cx.view().clone();
+            cx.subscribe(&view, move |_, _, event, _| {
                 if matches!(event, Event::Edited | Event::BufferEdited) {
                     events.borrow_mut().push(("editor1", event.clone()));
                 }
@@ -63,7 +64,7 @@ fn test_edit_events(cx: &mut TestAppContext) {
     let editor2 = cx.add_window({
         let events = events.clone();
         |cx| {
-            cx.subscribe(cx.view(), move |_, _, event, _| {
+            cx.subscribe(&cx.view().clone(), move |_, _, event, _| {
                 if matches!(event, Event::Edited | Event::BufferEdited) {
                     events.borrow_mut().push(("editor2", event.clone()));
                 }
@@ -156,7 +157,7 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
 
     let mut now = Instant::now();
     let buffer = cx.build_model(|cx| language::Buffer::new(0, cx.entity_id().as_u64(), "123456"));
-    let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval());
+    let group_interval = buffer.update(cx, |buffer, _| buffer.transaction_group_interval());
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
     let editor = cx.add_window(|cx| build_editor(buffer.clone(), cx));
 
@@ -496,10 +497,10 @@ fn test_clone(cx: &mut TestAppContext) {
     );
     assert_set_eq!(
         cloned_editor
-            .read_with(cx, |editor, cx| editor.selections.ranges::<Point>(cx))
+            .update(cx, |editor, cx| editor.selections.ranges::<Point>(cx))
             .unwrap(),
         editor
-            .read_with(cx, |editor, cx| editor.selections.ranges(cx))
+            .update(cx, |editor, cx| editor.selections.ranges(cx))
             .unwrap()
     );
     assert_set_eq!(
@@ -523,7 +524,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
     let project = Project::test(fs, [], cx).await;
     let workspace = cx.add_window(|cx| Workspace::test_new(project, cx));
     let pane = workspace
-        .read_with(cx, |workspace, _| workspace.active_pane().clone())
+        .update(cx, |workspace, _| workspace.active_pane().clone())
         .unwrap();
 
     workspace.update(cx, |v, cx| {
@@ -1279,357 +1280,358 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
     });
 }
 
-#[gpui::test]
-async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
-    init_test(cx, |_| {});
-    let mut cx = EditorTestContext::new(cx).await;
-
-    let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
-    let window = cx.window;
-    window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx);
-
-    cx.set_state(
-        &r#"ˇone
-        two
-
-        three
-        fourˇ
-        five
-
-        six"#
-            .unindent(),
-    );
-
-    cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
-    cx.assert_editor_state(
-        &r#"one
-        two
-        ˇ
-        three
-        four
-        five
-        ˇ
-        six"#
-            .unindent(),
-    );
-
-    cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
-    cx.assert_editor_state(
-        &r#"one
-        two
-
-        three
-        four
-        five
-        ˇ
-        sixˇ"#
-            .unindent(),
-    );
-
-    cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
-    cx.assert_editor_state(
-        &r#"one
-        two
-
-        three
-        four
-        five
-
-        sixˇ"#
-            .unindent(),
-    );
-
-    cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
-    cx.assert_editor_state(
-        &r#"one
-        two
-
-        three
-        four
-        five
-        ˇ
-        six"#
-            .unindent(),
-    );
-
-    cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
-    cx.assert_editor_state(
-        &r#"one
-        two
-        ˇ
-        three
-        four
-        five
-
-        six"#
-            .unindent(),
-    );
-
-    cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
-    cx.assert_editor_state(
-        &r#"ˇone
-        two
-
-        three
-        four
-        five
-
-        six"#
-            .unindent(),
-    );
-}
-
-#[gpui::test]
-async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
-    init_test(cx, |_| {});
-    let mut cx = EditorTestContext::new(cx).await;
-    let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
-    let window = cx.window;
-    window.simulate_resize(Point::new(1000., 4. * line_height + 0.5), &mut cx);
-
-    cx.set_state(
-        &r#"ˇone
-        two
-        three
-        four
-        five
-        six
-        seven
-        eight
-        nine
-        ten
-        "#,
-    );
-
-    cx.update_editor(|editor, cx| {
-        assert_eq!(
-            editor.snapshot(cx).scroll_position(),
-            gpui::Point::new(0., 0.)
-        );
-        editor.scroll_screen(&ScrollAmount::Page(1.), cx);
-        assert_eq!(
-            editor.snapshot(cx).scroll_position(),
-            gpui::Point::new(0., 3.)
-        );
-        editor.scroll_screen(&ScrollAmount::Page(1.), cx);
-        assert_eq!(
-            editor.snapshot(cx).scroll_position(),
-            gpui::Point::new(0., 6.)
-        );
-        editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
-        assert_eq!(
-            editor.snapshot(cx).scroll_position(),
-            gpui::Point::new(0., 3.)
-        );
-
-        editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
-        assert_eq!(
-            editor.snapshot(cx).scroll_position(),
-            gpui::Point::new(0., 1.)
-        );
-        editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
-        assert_eq!(
-            editor.snapshot(cx).scroll_position(),
-            gpui::Point::new(0., 3.)
-        );
-    });
-}
-
-#[gpui::test]
-async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
-    init_test(cx, |_| {});
-    let mut cx = EditorTestContext::new(cx).await;
-
-    let line_height = cx.update_editor(|editor, cx| {
-        editor.set_vertical_scroll_margin(2, cx);
-        editor.style(cx).text.line_height(cx.font_cache())
-    });
-
-    let window = cx.window;
-    window.simulate_resize(gpui::Point::new(1000., 6.0 * line_height), &mut cx);
-
-    cx.set_state(
-        &r#"ˇone
-            two
-            three
-            four
-            five
-            six
-            seven
-            eight
-            nine
-            ten
-        "#,
-    );
-    cx.update_editor(|editor, cx| {
-        assert_eq!(
-            editor.snapshot(cx).scroll_position(),
-            gpui::Point::new(0., 0.0)
-        );
-    });
-
-    // Add a cursor below the visible area. Since both cursors cannot fit
-    // on screen, the editor autoscrolls to reveal the newest cursor, and
-    // allows the vertical scroll margin below that cursor.
-    cx.update_editor(|editor, cx| {
-        editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
-            selections.select_ranges([
-                Point::new(0, 0)..Point::new(0, 0),
-                Point::new(6, 0)..Point::new(6, 0),
-            ]);
-        })
-    });
-    cx.update_editor(|editor, cx| {
-        assert_eq!(
-            editor.snapshot(cx).scroll_position(),
-            gpui::Point::new(0., 3.0)
-        );
-    });
-
-    // Move down. The editor cursor scrolls down to track the newest cursor.
-    cx.update_editor(|editor, cx| {
-        editor.move_down(&Default::default(), cx);
-    });
-    cx.update_editor(|editor, cx| {
-        assert_eq!(
-            editor.snapshot(cx).scroll_position(),
-            gpui::Point::new(0., 4.0)
-        );
-    });
-
-    // Add a cursor above the visible area. Since both cursors fit on screen,
-    // the editor scrolls to show both.
-    cx.update_editor(|editor, cx| {
-        editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
-            selections.select_ranges([
-                Point::new(1, 0)..Point::new(1, 0),
-                Point::new(6, 0)..Point::new(6, 0),
-            ]);
-        })
-    });
-    cx.update_editor(|editor, cx| {
-        assert_eq!(
-            editor.snapshot(cx).scroll_position(),
-            gpui::Point::new(0., 1.0)
-        );
-    });
-}
-
-#[gpui::test]
-async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
-    init_test(cx, |_| {});
-    let mut cx = EditorTestContext::new(cx).await;
-
-    let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
-    let window = cx.window;
-    window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx);
-
-    cx.set_state(
-        &r#"
-        ˇone
-        two
-        threeˇ
-        four
-        five
-        six
-        seven
-        eight
-        nine
-        ten
-        "#
-        .unindent(),
-    );
-
-    cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
-    cx.assert_editor_state(
-        &r#"
-        one
-        two
-        three
-        ˇfour
-        five
-        sixˇ
-        seven
-        eight
-        nine
-        ten
-        "#
-        .unindent(),
-    );
-
-    cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
-    cx.assert_editor_state(
-        &r#"
-        one
-        two
-        three
-        four
-        five
-        six
-        ˇseven
-        eight
-        nineˇ
-        ten
-        "#
-        .unindent(),
-    );
-
-    cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
-    cx.assert_editor_state(
-        &r#"
-        one
-        two
-        three
-        ˇfour
-        five
-        sixˇ
-        seven
-        eight
-        nine
-        ten
-        "#
-        .unindent(),
-    );
-
-    cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
-    cx.assert_editor_state(
-        &r#"
-        ˇone
-        two
-        threeˇ
-        four
-        five
-        six
-        seven
-        eight
-        nine
-        ten
-        "#
-        .unindent(),
-    );
-
-    // Test select collapsing
-    cx.update_editor(|editor, cx| {
-        editor.move_page_down(&MovePageDown::default(), cx);
-        editor.move_page_down(&MovePageDown::default(), cx);
-        editor.move_page_down(&MovePageDown::default(), cx);
-    });
-    cx.assert_editor_state(
-        &r#"
-        one
-        two
-        three
-        four
-        five
-        six
-        seven
-        eight
-        nine
-        ˇten
-        ˇ"#
-        .unindent(),
-    );
-}
+//todo!(simulate_resize)
+// #[gpui::test]
+// async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
+//     init_test(cx, |_| {});
+//     let mut cx = EditorTestContext::new(cx).await;
+
+//     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+//     let window = cx.window;
+//     window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx);
+
+//     cx.set_state(
+//         &r#"ˇone
+//         two
+
+//         three
+//         fourˇ
+//         five
+
+//         six"#
+//             .unindent(),
+//     );
+
+//     cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+//     cx.assert_editor_state(
+//         &r#"one
+//         two
+//         ˇ
+//         three
+//         four
+//         five
+//         ˇ
+//         six"#
+//             .unindent(),
+//     );
+
+//     cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+//     cx.assert_editor_state(
+//         &r#"one
+//         two
+
+//         three
+//         four
+//         five
+//         ˇ
+//         sixˇ"#
+//             .unindent(),
+//     );
+
+//     cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+//     cx.assert_editor_state(
+//         &r#"one
+//         two
+
+//         three
+//         four
+//         five
+
+//         sixˇ"#
+//             .unindent(),
+//     );
+
+//     cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+//     cx.assert_editor_state(
+//         &r#"one
+//         two
+
+//         three
+//         four
+//         five
+//         ˇ
+//         six"#
+//             .unindent(),
+//     );
+
+//     cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+//     cx.assert_editor_state(
+//         &r#"one
+//         two
+//         ˇ
+//         three
+//         four
+//         five
+
+//         six"#
+//             .unindent(),
+//     );
+
+//     cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+//     cx.assert_editor_state(
+//         &r#"ˇone
+//         two
+
+//         three
+//         four
+//         five
+
+//         six"#
+//             .unindent(),
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
+//     init_test(cx, |_| {});
+//     let mut cx = EditorTestContext::new(cx).await;
+//     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+//     let window = cx.window;
+//     window.simulate_resize(Point::new(1000., 4. * line_height + 0.5), &mut cx);
+
+//     cx.set_state(
+//         &r#"ˇone
+//         two
+//         three
+//         four
+//         five
+//         six
+//         seven
+//         eight
+//         nine
+//         ten
+//         "#,
+//     );
+
+//     cx.update_editor(|editor, cx| {
+//         assert_eq!(
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 0.)
+//         );
+//         editor.scroll_screen(&ScrollAmount::Page(1.), cx);
+//         assert_eq!(
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 3.)
+//         );
+//         editor.scroll_screen(&ScrollAmount::Page(1.), cx);
+//         assert_eq!(
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 6.)
+//         );
+//         editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
+//         assert_eq!(
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 3.)
+//         );
+
+//         editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
+//         assert_eq!(
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 1.)
+//         );
+//         editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
+//         assert_eq!(
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 3.)
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
+//     init_test(cx, |_| {});
+//     let mut cx = EditorTestContext::new(cx).await;
+
+//     let line_height = cx.update_editor(|editor, cx| {
+//         editor.set_vertical_scroll_margin(2, cx);
+//         editor.style(cx).text.line_height(cx.font_cache())
+//     });
+
+//     let window = cx.window;
+//     window.simulate_resize(gpui::Point::new(1000., 6.0 * line_height), &mut cx);
+
+//     cx.set_state(
+//         &r#"ˇone
+//             two
+//             three
+//             four
+//             five
+//             six
+//             seven
+//             eight
+//             nine
+//             ten
+//         "#,
+//     );
+//     cx.update_editor(|editor, cx| {
+//         assert_eq!(
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 0.0)
+//         );
+//     });
+
+//     // Add a cursor below the visible area. Since both cursors cannot fit
+//     // on screen, the editor autoscrolls to reveal the newest cursor, and
+//     // allows the vertical scroll margin below that cursor.
+//     cx.update_editor(|editor, cx| {
+//         editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
+//             selections.select_ranges([
+//                 Point::new(0, 0)..Point::new(0, 0),
+//                 Point::new(6, 0)..Point::new(6, 0),
+//             ]);
+//         })
+//     });
+//     cx.update_editor(|editor, cx| {
+//         assert_eq!(
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 3.0)
+//         );
+//     });
+
+//     // Move down. The editor cursor scrolls down to track the newest cursor.
+//     cx.update_editor(|editor, cx| {
+//         editor.move_down(&Default::default(), cx);
+//     });
+//     cx.update_editor(|editor, cx| {
+//         assert_eq!(
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 4.0)
+//         );
+//     });
+
+//     // Add a cursor above the visible area. Since both cursors fit on screen,
+//     // the editor scrolls to show both.
+//     cx.update_editor(|editor, cx| {
+//         editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
+//             selections.select_ranges([
+//                 Point::new(1, 0)..Point::new(1, 0),
+//                 Point::new(6, 0)..Point::new(6, 0),
+//             ]);
+//         })
+//     });
+//     cx.update_editor(|editor, cx| {
+//         assert_eq!(
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 1.0)
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
+//     init_test(cx, |_| {});
+//     let mut cx = EditorTestContext::new(cx).await;
+
+//     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+//     let window = cx.window;
+//     window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx);
+
+//     cx.set_state(
+//         &r#"
+//         ˇone
+//         two
+//         threeˇ
+//         four
+//         five
+//         six
+//         seven
+//         eight
+//         nine
+//         ten
+//         "#
+//         .unindent(),
+//     );
+
+//     cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
+//     cx.assert_editor_state(
+//         &r#"
+//         one
+//         two
+//         three
+//         ˇfour
+//         five
+//         sixˇ
+//         seven
+//         eight
+//         nine
+//         ten
+//         "#
+//         .unindent(),
+//     );
+
+//     cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
+//     cx.assert_editor_state(
+//         &r#"
+//         one
+//         two
+//         three
+//         four
+//         five
+//         six
+//         ˇseven
+//         eight
+//         nineˇ
+//         ten
+//         "#
+//         .unindent(),
+//     );
+
+//     cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
+//     cx.assert_editor_state(
+//         &r#"
+//         one
+//         two
+//         three
+//         ˇfour
+//         five
+//         sixˇ
+//         seven
+//         eight
+//         nine
+//         ten
+//         "#
+//         .unindent(),
+//     );
+
+//     cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
+//     cx.assert_editor_state(
+//         &r#"
+//         ˇone
+//         two
+//         threeˇ
+//         four
+//         five
+//         six
+//         seven
+//         eight
+//         nine
+//         ten
+//         "#
+//         .unindent(),
+//     );
+
+//     // Test select collapsing
+//     cx.update_editor(|editor, cx| {
+//         editor.move_page_down(&MovePageDown::default(), cx);
+//         editor.move_page_down(&MovePageDown::default(), cx);
+//         editor.move_page_down(&MovePageDown::default(), cx);
+//     });
+//     cx.assert_editor_state(
+//         &r#"
+//         one
+//         two
+//         three
+//         four
+//         five
+//         six
+//         seven
+//         eight
+//         nine
+//         ˇten
+//         ˇ"#
+//         .unindent(),
+//     );
+// }
 
 #[gpui::test]
 async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
@@ -3042,7 +3044,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
                 position: snapshot.anchor_after(Point::new(2, 0)),
                 disposition: BlockDisposition::Below,
                 height: 1,
-                render: Arc::new(|_| Empty::new().into_any()),
+                render: Arc::new(|_| div().render()),
             }],
             Some(Autoscroll::fit()),
             cx,
@@ -3148,245 +3150,246 @@ fn test_transpose(cx: &mut TestAppContext) {
     });
 }
 
+//todo!(clipboard)
+// #[gpui::test]
+// async fn test_clipboard(cx: &mut gpui::TestAppContext) {
+//     init_test(cx, |_| {});
+
+//     let mut cx = EditorTestContext::new(cx).await;
+
+//     cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
+//     cx.update_editor(|e, cx| e.cut(&Cut, cx));
+//     cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
+
+//     // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
+//     cx.set_state("two ˇfour ˇsix ˇ");
+//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
+//     cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
+
+//     // Paste again but with only two cursors. Since the number of cursors doesn't
+//     // match the number of slices in the clipboard, the entire clipboard text
+//     // is pasted at each cursor.
+//     cx.set_state("ˇtwo one✅ four three six five ˇ");
+//     cx.update_editor(|e, cx| {
+//         e.handle_input("( ", cx);
+//         e.paste(&Paste, cx);
+//         e.handle_input(") ", cx);
+//     });
+//     cx.assert_editor_state(
+//         &([
+//             "( one✅ ",
+//             "three ",
+//             "five ) ˇtwo one✅ four three six five ( one✅ ",
+//             "three ",
+//             "five ) ˇ",
+//         ]
+//         .join("\n")),
+//     );
+
+//     // Cut with three selections, one of which is full-line.
+//     cx.set_state(indoc! {"
+//         1«2ˇ»3
+//         4ˇ567
+//         «8ˇ»9"});
+//     cx.update_editor(|e, cx| e.cut(&Cut, cx));
+//     cx.assert_editor_state(indoc! {"
+//         1ˇ3
+//         ˇ9"});
+
+//     // Paste with three selections, noticing how the copied selection that was full-line
+//     // gets inserted before the second cursor.
+//     cx.set_state(indoc! {"
+//         1ˇ3
+//         9ˇ
+//         «oˇ»ne"});
+//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
+//     cx.assert_editor_state(indoc! {"
+//         12ˇ3
+//         4567
+//         9ˇ
+//         8ˇne"});
+
+//     // Copy with a single cursor only, which writes the whole line into the clipboard.
+//     cx.set_state(indoc! {"
+//         The quick brown
+//         fox juˇmps over
+//         the lazy dog"});
+//     cx.update_editor(|e, cx| e.copy(&Copy, cx));
+//     cx.cx.assert_clipboard_content(Some("fox jumps over\n"));
+
+//     // Paste with three selections, noticing how the copied full-line selection is inserted
+//     // before the empty selections but replaces the selection that is non-empty.
+//     cx.set_state(indoc! {"
+//         Tˇhe quick brown
+//         «foˇ»x jumps over
+//         tˇhe lazy dog"});
+//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
+//     cx.assert_editor_state(indoc! {"
+//         fox jumps over
+//         Tˇhe quick brown
+//         fox jumps over
+//         ˇx jumps over
+//         fox jumps over
+//         tˇhe lazy dog"});
+// }
+
+// #[gpui::test]
+// async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
+//     init_test(cx, |_| {});
+
+//     let mut cx = EditorTestContext::new(cx).await;
+//     let language = Arc::new(Language::new(
+//         LanguageConfig::default(),
+//         Some(tree_sitter_rust::language()),
+//     ));
+//     cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+//     // Cut an indented block, without the leading whitespace.
+//     cx.set_state(indoc! {"
+//         const a: B = (
+//             c(),
+//             «d(
+//                 e,
+//                 f
+//             )ˇ»
+//         );
+//     "});
+//     cx.update_editor(|e, cx| e.cut(&Cut, cx));
+//     cx.assert_editor_state(indoc! {"
+//         const a: B = (
+//             c(),
+//             ˇ
+//         );
+//     "});
+
+//     // Paste it at the same position.
+//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
+//     cx.assert_editor_state(indoc! {"
+//         const a: B = (
+//             c(),
+//             d(
+//                 e,
+//                 f
+//             )ˇ
+//         );
+//     "});
+
+//     // Paste it at a line with a lower indent level.
+//     cx.set_state(indoc! {"
+//         ˇ
+//         const a: B = (
+//             c(),
+//         );
+//     "});
+//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
+//     cx.assert_editor_state(indoc! {"
+//         d(
+//             e,
+//             f
+//         )ˇ
+//         const a: B = (
+//             c(),
+//         );
+//     "});
+
+//     // Cut an indented block, with the leading whitespace.
+//     cx.set_state(indoc! {"
+//         const a: B = (
+//             c(),
+//         «    d(
+//                 e,
+//                 f
+//             )
+//         ˇ»);
+//     "});
+//     cx.update_editor(|e, cx| e.cut(&Cut, cx));
+//     cx.assert_editor_state(indoc! {"
+//         const a: B = (
+//             c(),
+//         ˇ);
+//     "});
+
+//     // Paste it at the same position.
+//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
+//     cx.assert_editor_state(indoc! {"
+//         const a: B = (
+//             c(),
+//             d(
+//                 e,
+//                 f
+//             )
+//         ˇ);
+//     "});
+
+//     // Paste it at a line with a higher indent level.
+//     cx.set_state(indoc! {"
+//         const a: B = (
+//             c(),
+//             d(
+//                 e,
+//                 fˇ
+//             )
+//         );
+//     "});
+//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
+//     cx.assert_editor_state(indoc! {"
+//         const a: B = (
+//             c(),
+//             d(
+//                 e,
+//                 f    d(
+//                     e,
+//                     f
+//                 )
+//         ˇ
+//             )
+//         );
+//     "});
+// }
+
 #[gpui::test]
-async fn test_clipboard(cx: &mut gpui::TestAppContext) {
+fn test_select_all(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let mut cx = EditorTestContext::new(cx).await;
-
-    cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
-    cx.update_editor(|e, cx| e.cut(&Cut, cx));
-    cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
-
-    // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
-    cx.set_state("two ˇfour ˇsix ˇ");
-    cx.update_editor(|e, cx| e.paste(&Paste, cx));
-    cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
-
-    // Paste again but with only two cursors. Since the number of cursors doesn't
-    // match the number of slices in the clipboard, the entire clipboard text
-    // is pasted at each cursor.
-    cx.set_state("ˇtwo one✅ four three six five ˇ");
-    cx.update_editor(|e, cx| {
-        e.handle_input("( ", cx);
-        e.paste(&Paste, cx);
-        e.handle_input(") ", cx);
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
+        build_editor(buffer, cx)
+    });
+    view.update(cx, |view, cx| {
+        view.select_all(&SelectAll, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)]
+        );
     });
-    cx.assert_editor_state(
-        &([
-            "( one✅ ",
-            "three ",
-            "five ) ˇtwo one✅ four three six five ( one✅ ",
-            "three ",
-            "five ) ˇ",
-        ]
-        .join("\n")),
-    );
-
-    // Cut with three selections, one of which is full-line.
-    cx.set_state(indoc! {"
-        1«2ˇ»3
-        4ˇ567
-        «8ˇ»9"});
-    cx.update_editor(|e, cx| e.cut(&Cut, cx));
-    cx.assert_editor_state(indoc! {"
-        1ˇ3
-        ˇ9"});
-
-    // Paste with three selections, noticing how the copied selection that was full-line
-    // gets inserted before the second cursor.
-    cx.set_state(indoc! {"
-        1ˇ3
-        9ˇ
-        «oˇ»ne"});
-    cx.update_editor(|e, cx| e.paste(&Paste, cx));
-    cx.assert_editor_state(indoc! {"
-        12ˇ3
-        4567
-        9ˇ
-        8ˇne"});
-
-    // Copy with a single cursor only, which writes the whole line into the clipboard.
-    cx.set_state(indoc! {"
-        The quick brown
-        fox juˇmps over
-        the lazy dog"});
-    cx.update_editor(|e, cx| e.copy(&Copy, cx));
-    cx.cx.assert_clipboard_content(Some("fox jumps over\n"));
-
-    // Paste with three selections, noticing how the copied full-line selection is inserted
-    // before the empty selections but replaces the selection that is non-empty.
-    cx.set_state(indoc! {"
-        Tˇhe quick brown
-        «foˇ»x jumps over
-        tˇhe lazy dog"});
-    cx.update_editor(|e, cx| e.paste(&Paste, cx));
-    cx.assert_editor_state(indoc! {"
-        fox jumps over
-        Tˇhe quick brown
-        fox jumps over
-        ˇx jumps over
-        fox jumps over
-        tˇhe lazy dog"});
 }
 
 #[gpui::test]
-async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
+fn test_select_line(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let mut cx = EditorTestContext::new(cx).await;
-    let language = Arc::new(Language::new(
-        LanguageConfig::default(),
-        Some(tree_sitter_rust::language()),
-    ));
-    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
-
-    // Cut an indented block, without the leading whitespace.
-    cx.set_state(indoc! {"
-        const a: B = (
-            c(),
-            «d(
-                e,
-                f
-            )ˇ»
-        );
-    "});
-    cx.update_editor(|e, cx| e.cut(&Cut, cx));
-    cx.assert_editor_state(indoc! {"
-        const a: B = (
-            c(),
-            ˇ
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
+        build_editor(buffer, cx)
+    });
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+                DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2),
+            ])
+        });
+        view.select_line(&SelectLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0),
+                DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0),
+            ]
         );
-    "});
-
-    // Paste it at the same position.
-    cx.update_editor(|e, cx| e.paste(&Paste, cx));
-    cx.assert_editor_state(indoc! {"
-        const a: B = (
-            c(),
-            d(
-                e,
-                f
-            )ˇ
-        );
-    "});
-
-    // Paste it at a line with a lower indent level.
-    cx.set_state(indoc! {"
-        ˇ
-        const a: B = (
-            c(),
-        );
-    "});
-    cx.update_editor(|e, cx| e.paste(&Paste, cx));
-    cx.assert_editor_state(indoc! {"
-        d(
-            e,
-            f
-        )ˇ
-        const a: B = (
-            c(),
-        );
-    "});
-
-    // Cut an indented block, with the leading whitespace.
-    cx.set_state(indoc! {"
-        const a: B = (
-            c(),
-        «    d(
-                e,
-                f
-            )
-        ˇ»);
-    "});
-    cx.update_editor(|e, cx| e.cut(&Cut, cx));
-    cx.assert_editor_state(indoc! {"
-        const a: B = (
-            c(),
-        ˇ);
-    "});
-
-    // Paste it at the same position.
-    cx.update_editor(|e, cx| e.paste(&Paste, cx));
-    cx.assert_editor_state(indoc! {"
-        const a: B = (
-            c(),
-            d(
-                e,
-                f
-            )
-        ˇ);
-    "});
-
-    // Paste it at a line with a higher indent level.
-    cx.set_state(indoc! {"
-        const a: B = (
-            c(),
-            d(
-                e,
-                fˇ
-            )
-        );
-    "});
-    cx.update_editor(|e, cx| e.paste(&Paste, cx));
-    cx.assert_editor_state(indoc! {"
-        const a: B = (
-            c(),
-            d(
-                e,
-                f    d(
-                    e,
-                    f
-                )
-        ˇ
-            )
-        );
-    "});
-}
-
-#[gpui::test]
-fn test_select_all(cx: &mut TestAppContext) {
-    init_test(cx, |_| {});
-
-    let view = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
-        build_editor(buffer, cx)
-    });
-    view.update(cx, |view, cx| {
-        view.select_all(&SelectAll, cx);
-        assert_eq!(
-            view.selections.display_ranges(cx),
-            &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)]
-        );
-    });
-}
-
-#[gpui::test]
-fn test_select_line(cx: &mut TestAppContext) {
-    init_test(cx, |_| {});
-
-    let view = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
-        build_editor(buffer, cx)
-    });
-    view.update(cx, |view, cx| {
-        view.change_selections(None, cx, |s| {
-            s.select_display_ranges([
-                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
-                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
-                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
-                DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2),
-            ])
-        });
-        view.select_line(&SelectLine, cx);
-        assert_eq!(
-            view.selections.display_ranges(cx),
-            vec![
-                DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0),
-                DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0),
-            ]
-        );
-    });
+    });
 
     view.update(cx, |view, cx| {
         view.select_line(&SelectLine, cx);

crates/editor2/src/test/editor_lsp_test_context.rs 🔗

@@ -10,7 +10,7 @@ use serde_json::json;
 use crate::{Editor, ToPoint};
 use collections::HashSet;
 use futures::Future;
-use gpui::{json, View, ViewContext};
+use gpui::{View, ViewContext, VisualTestContext};
 use indoc::indoc;
 use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
 use lsp::{notification, request};
@@ -19,7 +19,7 @@ use project::Project;
 use smol::stream::StreamExt;
 use workspace::{AppState, Workspace, WorkspaceHandle};
 
-use super::editor_test_context::EditorTestContext;
+use super::editor_test_context::{AssertionContextManager, EditorTestContext};
 
 pub struct EditorLspTestContext<'a> {
     pub cx: EditorTestContext<'a>,
@@ -34,8 +34,6 @@ impl<'a> EditorLspTestContext<'a> {
         capabilities: lsp::ServerCapabilities,
         cx: &'a mut gpui::TestAppContext,
     ) -> EditorLspTestContext<'a> {
-        use json::json;
-
         let app_state = cx.update(AppState::test);
 
         cx.update(|cx| {
@@ -70,9 +68,10 @@ impl<'a> EditorLspTestContext<'a> {
             .await;
 
         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-        let workspace = window.root(cx);
+        let workspace = window.root_view(cx).unwrap();
+        let mut cx = VisualTestContext::from_window(*window.deref(), cx);
         project
-            .update(cx, |project, cx| {
+            .update(&mut cx, |project, cx| {
                 project.find_or_create_local_worktree("/root", true, cx)
             })
             .await
@@ -82,7 +81,7 @@ impl<'a> EditorLspTestContext<'a> {
 
         let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
         let item = workspace
-            .update(cx, |workspace, cx| {
+            .update(&mut cx, |workspace, cx| {
                 workspace.open_path(file, None, true, cx)
             })
             .await
@@ -92,7 +91,7 @@ impl<'a> EditorLspTestContext<'a> {
             item.act_as::<Editor>(cx)
                 .expect("Opened test file wasn't an editor")
         });
-        editor.update(cx, |_, cx| cx.focus_self());
+        editor.update(&mut cx, |editor, cx| editor.focus(cx));
 
         let lsp = fake_servers.next().await.unwrap();
 
@@ -101,6 +100,7 @@ impl<'a> EditorLspTestContext<'a> {
                 cx,
                 window: window.into(),
                 editor,
+                assertion_cx: AssertionContextManager::new(),
             },
             lsp,
             workspace,
@@ -258,7 +258,7 @@ impl<'a> EditorLspTestContext<'a> {
     where
         F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
     {
-        self.workspace.update(self.cx.cx, update)
+        self.workspace.update(&mut self.cx.cx, update)
     }
 
     pub fn handle_request<T, F, Fut>(

crates/editor2/src/test/editor_test_context.rs 🔗

@@ -1,28 +1,37 @@
 use crate::{
     display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
 };
+use collections::BTreeMap;
 use futures::Future;
 use gpui::{
     AnyWindowHandle, AppContext, ForegroundExecutor, Keystroke, ModelContext, View, ViewContext,
+    VisualTestContext, WindowHandle,
 };
 use indoc::indoc;
+use itertools::Itertools;
 use language::{Buffer, BufferSnapshot};
+use parking_lot::RwLock;
 use project::{FakeFs, Project};
 use std::{
     any::TypeId,
     ops::{Deref, DerefMut, Range},
+    sync::{
+        atomic::{AtomicUsize, Ordering},
+        Arc,
+    },
 };
 use util::{
     assert_set_eq,
     test::{generate_marked_text, marked_text_ranges},
 };
 
-// use super::build_editor_with_project;
+use super::build_editor_with_project;
 
 pub struct EditorTestContext<'a> {
-    pub cx: &'a mut gpui::TestAppContext,
+    pub cx: gpui::VisualTestContext<'a>,
     pub window: AnyWindowHandle,
     pub editor: View<Editor>,
+    pub assertion_cx: AssertionContextManager,
 }
 
 impl<'a> EditorTestContext<'a> {
@@ -43,15 +52,18 @@ impl<'a> EditorTestContext<'a> {
             })
             .await
             .unwrap();
-        let window = cx.add_window(|cx| {
-            cx.focus_self();
-            build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx)
+        let editor = cx.add_window(|cx| {
+            let editor =
+                build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx);
+            editor.focus(cx);
+            editor
         });
-        let editor = window.root(cx);
+        let editor_view = editor.root_view(cx).unwrap();
         Self {
-            cx,
-            window: window.into(),
-            editor,
+            cx: VisualTestContext::from_window(*editor.deref(), cx),
+            window: editor.into(),
+            editor: editor_view,
+            assertion_cx: AssertionContextManager::new(),
         }
     }
 
@@ -59,24 +71,27 @@ impl<'a> EditorTestContext<'a> {
         &self,
         predicate: impl FnMut(&Editor, &AppContext) -> bool,
     ) -> impl Future<Output = ()> {
-        self.editor.condition(self.cx, predicate)
+        self.editor.condition::<crate::Event>(&self.cx, predicate)
     }
 
-    pub fn editor<F, T>(&self, read: F) -> T
+    #[track_caller]
+    pub fn editor<F, T>(&mut self, read: F) -> T
     where
         F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
     {
-        self.editor.update(self.cx, read)
+        self.editor
+            .update(&mut self.cx, |this, cx| read(&this, &cx))
     }
 
+    #[track_caller]
     pub fn update_editor<F, T>(&mut self, update: F) -> T
     where
         F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
     {
-        self.editor.update(self.cx, update)
+        self.editor.update(&mut self.cx, update)
     }
 
-    pub fn multibuffer<F, T>(&self, read: F) -> T
+    pub fn multibuffer<F, T>(&mut self, read: F) -> T
     where
         F: FnOnce(&MultiBuffer, &AppContext) -> T,
     {
@@ -90,11 +105,11 @@ impl<'a> EditorTestContext<'a> {
         self.update_editor(|editor, cx| editor.buffer().update(cx, update))
     }
 
-    pub fn buffer_text(&self) -> String {
+    pub fn buffer_text(&mut self) -> String {
         self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
     }
 
-    pub fn buffer<F, T>(&self, read: F) -> T
+    pub fn buffer<F, T>(&mut self, read: F) -> T
     where
         F: FnOnce(&Buffer, &AppContext) -> T,
     {
@@ -114,10 +129,18 @@ impl<'a> EditorTestContext<'a> {
         })
     }
 
-    pub fn buffer_snapshot(&self) -> BufferSnapshot {
+    pub fn buffer_snapshot(&mut self) -> BufferSnapshot {
         self.buffer(|buffer, _| buffer.snapshot())
     }
 
+    pub fn add_assertion_context(&self, context: String) -> ContextHandle {
+        self.assertion_cx.add_context(context)
+    }
+
+    pub fn assertion_context(&self) -> String {
+        self.assertion_cx.context()
+    }
+
     pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
         let keystroke_under_test_handle =
             self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
@@ -141,16 +164,12 @@ impl<'a> EditorTestContext<'a> {
         // before returning.
         // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
         // quickly races with async actions.
-        if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() {
-            executor.run_until_parked();
-        } else {
-            unreachable!();
-        }
+        self.cx.background_executor.run_until_parked();
 
         keystrokes_under_test_handle
     }
 
-    pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
+    pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
         let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
         assert_eq!(self.buffer_text(), unmarked_text);
         ranges
@@ -160,12 +179,12 @@ impl<'a> EditorTestContext<'a> {
         let ranges = self.ranges(marked_text);
         let snapshot = self
             .editor
-            .update(self.cx, |editor, cx| editor.snapshot(cx));
+            .update(&mut self.cx, |editor, cx| editor.snapshot(cx));
         ranges[0].start.to_display_point(&snapshot)
     }
 
     // Returns anchors for the current buffer using `«` and `»`
-    pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
+    pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
         let ranges = self.ranges(marked_text);
         let snapshot = self.buffer_snapshot();
         snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
@@ -190,7 +209,7 @@ impl<'a> EditorTestContext<'a> {
             marked_text.escape_debug().to_string()
         ));
         let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
-        self.editor.update(self.cx, |editor, cx| {
+        self.editor.update(&mut self.cx, |editor, cx| {
             editor.set_text(unmarked_text, cx);
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                 s.select_ranges(selection_ranges)
@@ -206,7 +225,7 @@ impl<'a> EditorTestContext<'a> {
             marked_text.escape_debug().to_string()
         ));
         let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
-        self.editor.update(self.cx, |editor, cx| {
+        self.editor.update(&mut self.cx, |editor, cx| {
             assert_eq!(editor.text(cx), unmarked_text);
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                 s.select_ranges(selection_ranges)
@@ -273,9 +292,12 @@ impl<'a> EditorTestContext<'a> {
         self.assert_selections(expected_selections, expected_marked_text)
     }
 
-    fn editor_selections(&self) -> Vec<Range<usize>> {
+    #[track_caller]
+    fn editor_selections(&mut self) -> Vec<Range<usize>> {
         self.editor
-            .read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
+            .update(&mut self.cx, |editor, cx| {
+                editor.selections.all::<usize>(cx)
+            })
             .into_iter()
             .map(|s| {
                 if s.reversed {
@@ -320,7 +342,7 @@ impl<'a> Deref for EditorTestContext<'a> {
     type Target = gpui::TestAppContext;
 
     fn deref(&self) -> &Self::Target {
-        self.cx
+        &self.cx
     }
 }
 
@@ -329,3 +351,50 @@ impl<'a> DerefMut for EditorTestContext<'a> {
         &mut self.cx
     }
 }
+
+/// Tracks string context to be printed when assertions fail.
+/// Often this is done by storing a context string in the manager and returning the handle.
+#[derive(Clone)]
+pub struct AssertionContextManager {
+    id: Arc<AtomicUsize>,
+    contexts: Arc<RwLock<BTreeMap<usize, String>>>,
+}
+
+impl AssertionContextManager {
+    pub fn new() -> Self {
+        Self {
+            id: Arc::new(AtomicUsize::new(0)),
+            contexts: Arc::new(RwLock::new(BTreeMap::new())),
+        }
+    }
+
+    pub fn add_context(&self, context: String) -> ContextHandle {
+        let id = self.id.fetch_add(1, Ordering::Relaxed);
+        let mut contexts = self.contexts.write();
+        contexts.insert(id, context);
+        ContextHandle {
+            id,
+            manager: self.clone(),
+        }
+    }
+
+    pub fn context(&self) -> String {
+        let contexts = self.contexts.read();
+        format!("\n{}\n", contexts.values().join("\n"))
+    }
+}
+
+/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
+/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
+/// the state that was set initially for the failure can be printed in the error message
+pub struct ContextHandle {
+    id: usize,
+    manager: AssertionContextManager,
+}
+
+impl Drop for ContextHandle {
+    fn drop(&mut self) {
+        let mut contexts = self.manager.contexts.write();
+        contexts.remove(&self.id);
+    }
+}

crates/gpui2/src/app/test_context.rs 🔗

@@ -6,7 +6,7 @@ use crate::{
 };
 use anyhow::{anyhow, bail};
 use futures::{Stream, StreamExt};
-use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
+use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
 
 #[derive(Clone)]
 pub struct TestAppContext {
@@ -132,6 +132,18 @@ impl TestAppContext {
         cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window))
     }
 
+    pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, VisualTestContext)
+    where
+        F: FnOnce(&mut ViewContext<V>) -> V,
+        V: Render,
+    {
+        let mut cx = self.app.borrow_mut();
+        let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
+        drop(cx);
+        let view = window.root_view(self).unwrap();
+        (view, VisualTestContext::from_window(*window.deref(), self))
+    }
+
     pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
     where
         Fut: Future<Output = R> + 'static,
@@ -158,7 +170,7 @@ impl TestAppContext {
         Some(read(lock.try_global()?, &lock))
     }
 
-    pub fn set_global<G: 'static, R>(&mut self, global: G) {
+    pub fn set_global<G: 'static>(&mut self, global: G) {
         let mut lock = self.app.borrow_mut();
         lock.set_global(global);
     }
@@ -277,6 +289,72 @@ impl<T: Send> Model<T> {
     }
 }
 
+impl<V> View<V> {
+    pub fn condition<Evt>(
+        &self,
+        cx: &TestAppContext,
+        mut predicate: impl FnMut(&V, &AppContext) -> bool,
+    ) -> impl Future<Output = ()>
+    where
+        Evt: 'static,
+        V: EventEmitter<Evt>,
+    {
+        use postage::prelude::{Sink as _, Stream as _};
+
+        let (tx, mut rx) = postage::mpsc::channel(1024);
+        let timeout_duration = Duration::from_millis(100); //todo!() cx.condition_duration();
+
+        let mut cx = cx.app.borrow_mut();
+        let subscriptions = (
+            cx.observe(self, {
+                let mut tx = tx.clone();
+                move |_, _| {
+                    tx.blocking_send(()).ok();
+                }
+            }),
+            cx.subscribe(self, {
+                let mut tx = tx.clone();
+                move |_, _: &Evt, _| {
+                    tx.blocking_send(()).ok();
+                }
+            }),
+        );
+
+        let cx = cx.this.upgrade().unwrap();
+        let handle = self.downgrade();
+
+        async move {
+            crate::util::timeout(timeout_duration, async move {
+                loop {
+                    {
+                        let cx = cx.borrow();
+                        let cx = &*cx;
+                        if predicate(
+                            handle
+                                .upgrade()
+                                .expect("view dropped with pending condition")
+                                .read(cx),
+                            cx,
+                        ) {
+                            break;
+                        }
+                    }
+
+                    // todo!(start_waiting)
+                    // cx.borrow().foreground_executor().start_waiting();
+                    rx.recv()
+                        .await
+                        .expect("view dropped with pending condition");
+                    // cx.borrow().foreground_executor().finish_waiting();
+                }
+            })
+            .await
+            .expect("condition timed out");
+            drop(subscriptions);
+        }
+    }
+}
+
 use derive_more::{Deref, DerefMut};
 #[derive(Deref, DerefMut)]
 pub struct VisualTestContext<'a> {

crates/gpui2/src/color.rs 🔗

@@ -167,7 +167,7 @@ impl TryFrom<&'_ str> for Rgba {
     }
 }
 
-#[derive(Default, Copy, Clone, Debug, PartialEq, PartialOrd)]
+#[derive(Default, Copy, Clone, Debug)]
 #[repr(C)]
 pub struct Hsla {
     pub h: f32,
@@ -176,6 +176,35 @@ pub struct Hsla {
     pub a: f32,
 }
 
+impl PartialEq for Hsla {
+    fn eq(&self, other: &Self) -> bool {
+        self.h
+            .total_cmp(&other.h)
+            .then(self.s.total_cmp(&other.s))
+            .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
+            .is_eq()
+    }
+}
+
+impl PartialOrd for Hsla {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        // SAFETY: The total ordering relies on this always being Some()
+        Some(
+            self.h
+                .total_cmp(&other.h)
+                .then(self.s.total_cmp(&other.s))
+                .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))),
+        )
+    }
+}
+
+impl Ord for Hsla {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        // SAFETY: The partial comparison is a total comparison
+        unsafe { self.partial_cmp(other).unwrap_unchecked() }
+    }
+}
+
 impl Hsla {
     pub fn to_rgb(self) -> Rgba {
         self.into()

crates/gpui2/src/util.rs 🔗

@@ -1,16 +1,20 @@
+use std::time::Duration;
+
+use futures::Future;
+use smol::future::FutureExt;
 pub use util::*;
 
-// pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
-// where
-//     F: Future<Output = T>,
-// {
-//     let timer = async {
-//         smol::Timer::after(timeout).await;
-//         Err(())
-//     };
-//     let future = async move { Ok(f.await) };
-//     timer.race(future).await
-// }
+pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
+where
+    F: Future<Output = T>,
+{
+    let timer = async {
+        smol::Timer::after(timeout).await;
+        Err(())
+    };
+    let future = async move { Ok(f.await) };
+    timer.race(future).await
+}
 
 #[cfg(any(test, feature = "test-support"))]
 pub struct CwdBacktrace<'a>(pub &'a backtrace::Backtrace);

crates/workspace2/src/workspace2.rs 🔗

@@ -4200,24 +4200,24 @@ impl ViewId {
     }
 }
 
-// pub trait WorkspaceHandle {
-//     fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
-// }
+pub trait WorkspaceHandle {
+    fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
+}
 
-// impl WorkspaceHandle for View<Workspace> {
-//     fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
-//         self.read(cx)
-//             .worktrees(cx)
-//             .flat_map(|worktree| {
-//                 let worktree_id = worktree.read(cx).id();
-//                 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
-//                     worktree_id,
-//                     path: f.path.clone(),
-//                 })
-//             })
-//             .collect::<Vec<_>>()
-//     }
-// }
+impl WorkspaceHandle for View<Workspace> {
+    fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
+        self.read(cx)
+            .worktrees(cx)
+            .flat_map(|worktree| {
+                let worktree_id = worktree.read(cx).id();
+                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
+                    worktree_id,
+                    path: f.path.clone(),
+                })
+            })
+            .collect::<Vec<_>>()
+    }
+}
 
 // impl std::fmt::Debug for OpenPaths {
 //     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {