Add invisibles wrapping test

Kirill Bulatov created

Change summary

crates/editor/src/element.rs | 213 ++++++++++++++++++++++++-------------
1 file changed, 135 insertions(+), 78 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -1711,16 +1711,6 @@ enum Invisible {
     Whitespace { line_offset: usize },
 }
 
-impl Invisible {
-    #[cfg(test)]
-    fn offset(&self) -> usize {
-        *match self {
-            Self::Tab { line_start_offset } => line_start_offset,
-            Self::Whitespace { line_offset } => line_offset,
-        }
-    }
-}
-
 fn layout_highlighted_chunks<'a>(
     chunks: impl Iterator<Item = HighlightedChunk<'a>>,
     text_style: &TextStyle,
@@ -2781,6 +2771,7 @@ mod tests {
         Editor, MultiBuffer,
     };
     use gpui::TestAppContext;
+    use log::info;
     use settings::Settings;
     use std::{num::NonZeroU32, sync::Arc};
     use util::test::sample_text;
@@ -2864,18 +2855,21 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_both_invisible_kinds_drawing(cx: &mut TestAppContext) {
+    fn test_all_invisibles_drawing(cx: &mut TestAppContext) {
         let tab_size = 4;
-        let input_text = "\t\t\t| | a b";
+        let input_text = "\t \t|\t| a b";
         let expected_invisibles = vec![
             Invisible::Tab {
                 line_start_offset: 0,
             },
+            Invisible::Whitespace {
+                line_offset: tab_size as usize,
+            },
             Invisible::Tab {
-                line_start_offset: tab_size as usize,
+                line_start_offset: tab_size as usize + 1,
             },
             Invisible::Tab {
-                line_start_offset: tab_size as usize * 2,
+                line_start_offset: tab_size as usize * 2 + 1,
             },
             Invisible::Whitespace {
                 line_offset: tab_size as usize * 3 + 1,
@@ -2883,16 +2877,14 @@ mod tests {
             Invisible::Whitespace {
                 line_offset: tab_size as usize * 3 + 3,
             },
-            Invisible::Whitespace {
-                line_offset: tab_size as usize * 3 + 5,
-            },
         ];
         assert_eq!(
             expected_invisibles.len(),
             input_text
                 .chars()
                 .filter(|initial_char| initial_char.is_whitespace())
-                .count()
+                .count(),
+            "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
         );
 
         cx.update(|cx| {
@@ -2901,13 +2893,132 @@ mod tests {
             test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap());
             cx.set_global(test_settings);
         });
+        let actual_invisibles =
+            collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0);
+
+        assert_eq!(expected_invisibles, actual_invisibles);
+    }
+
+    #[gpui::test]
+    fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            let mut test_settings = Settings::test(cx);
+            test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All);
+            test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(4).unwrap());
+            cx.set_global(test_settings);
+        });
+
+        for editor_mode_without_invisibles in [
+            EditorMode::SingleLine,
+            EditorMode::AutoHeight { max_lines: 100 },
+        ] {
+            let invisibles = collect_invisibles_from_new_editor(
+                cx,
+                editor_mode_without_invisibles,
+                "\t\t\t| | a b",
+                500.0,
+            );
+            assert!(invisibles.is_empty(),
+                "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}");
+        }
+    }
+
+    #[gpui::test]
+    fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
+        let tab_size = 4;
+        let input_text = "a\tbcd   ".repeat(9);
+        let repeated_invisibles = [
+            Invisible::Tab {
+                line_start_offset: 1,
+            },
+            Invisible::Whitespace {
+                line_offset: tab_size as usize + 3,
+            },
+            Invisible::Whitespace {
+                line_offset: tab_size as usize + 4,
+            },
+            Invisible::Whitespace {
+                line_offset: tab_size as usize + 5,
+            },
+        ];
+        let expected_invisibles = std::iter::once(repeated_invisibles)
+            .cycle()
+            .take(9)
+            .flatten()
+            .collect::<Vec<_>>();
+        assert_eq!(
+            expected_invisibles.len(),
+            input_text
+                .chars()
+                .filter(|initial_char| initial_char.is_whitespace())
+                .count(),
+            "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
+        );
+        info!("Expected invisibles: {expected_invisibles:?}");
+
+        // Put the same string with repeating whitespace pattern into editors of various size,
+        // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point.
+        let resize_step = 10.0;
+        let mut editor_width = 200.0;
+        while editor_width <= 1000.0 {
+            cx.update(|cx| {
+                let mut test_settings = Settings::test(cx);
+                test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap());
+                test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All);
+                test_settings.editor_defaults.preferred_line_length = Some(editor_width as u32);
+                test_settings.editor_defaults.soft_wrap =
+                    Some(settings::SoftWrap::PreferredLineLength);
+                cx.set_global(test_settings);
+            });
+
+            let actual_invisibles =
+                collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, editor_width);
+
+            // Whatever the editor size is, ensure it has the same invisible kinds in the same order
+            // (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
+            let mut i = 0;
+            for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
+                i = actual_index;
+                match expected_invisibles.get(i) {
+                    Some(expected_invisible) => match (expected_invisible, actual_invisible) {
+                        (Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
+                        | (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
+                        _ => {
+                            panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}")
+                        }
+                    },
+                    None => panic!("Unexpected extra invisible {actual_invisible:?} at index {i}"),
+                }
+            }
+            let missing_expected_invisibles = &expected_invisibles[i + 1..];
+            assert!(
+                missing_expected_invisibles.is_empty(),
+                "Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
+            );
+
+            editor_width += resize_step;
+        }
+    }
+
+    fn collect_invisibles_from_new_editor(
+        cx: &mut TestAppContext,
+        editor_mode: EditorMode,
+        input_text: &str,
+        editor_width: f32,
+    ) -> Vec<Invisible> {
+        info!(
+            "Creating editor with mode {editor_mode:?}, witdh {editor_width} and text '{input_text}'"
+        );
         let (_, editor) = cx.add_window(|cx| {
             let buffer = MultiBuffer::build_simple(&input_text, cx);
-            Editor::new(EditorMode::Full, buffer, None, None, cx)
+            Editor::new(editor_mode, buffer, None, None, cx)
         });
 
         let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
         let (_, layout_state) = editor.update(cx, |editor, cx| {
+            editor.set_soft_wrap_mode(settings::SoftWrap::EditorWidth, cx);
+            editor.set_wrap_width(Some(editor_width), cx);
+
             let mut new_parents = Default::default();
             let mut notify_views_if_parents_change = Default::default();
             let mut layout_cx = LayoutContext::new(
@@ -2917,73 +3028,19 @@ mod tests {
                 false,
             );
             element.layout(
-                SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
+                SizeConstraint::new(vec2f(editor_width, 500.), vec2f(editor_width, 500.)),
                 editor,
                 &mut layout_cx,
             )
         });
 
-        let line_layouts = &layout_state.position_map.line_layouts;
-        let actual_invisibles = line_layouts
+        layout_state
+            .position_map
+            .line_layouts
             .iter()
             .map(|line_with_invisibles| &line_with_invisibles.invisibles)
             .flatten()
-            .sorted_by(|invisible_1, invisible_2| invisible_1.offset().cmp(&invisible_2.offset()))
             .cloned()
-            .collect::<Vec<_>>();
-
-        assert_eq!(expected_invisibles, actual_invisibles);
-    }
-
-    #[gpui::test]
-    fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
-        cx.update(|cx| {
-            let mut test_settings = Settings::test(cx);
-            test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All);
-            test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(4).unwrap());
-            cx.set_global(test_settings);
-        });
-
-        for editor_mode_without_invisibles in [
-            EditorMode::SingleLine,
-            EditorMode::AutoHeight { max_lines: 100 },
-        ] {
-            let (_, editor) = cx.add_window(|cx| {
-                let buffer = MultiBuffer::build_simple("\t\t\t| | a b", cx);
-                Editor::new(editor_mode_without_invisibles, buffer, None, None, cx)
-            });
-
-            let mut element =
-                EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
-            let (_, layout_state) = editor.update(cx, |editor, cx| {
-                let mut new_parents = Default::default();
-                let mut notify_views_if_parents_change = Default::default();
-                let mut layout_cx = LayoutContext::new(
-                    cx,
-                    &mut new_parents,
-                    &mut notify_views_if_parents_change,
-                    false,
-                );
-                element.layout(
-                    SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
-                    editor,
-                    &mut layout_cx,
-                )
-            });
-
-            let line_layouts = &layout_state.position_map.line_layouts;
-            let invisibles = line_layouts
-                .iter()
-                .map(|line_with_invisibles| &line_with_invisibles.invisibles)
-                .flatten()
-                .sorted_by(|invisible_1, invisible_2| {
-                    invisible_1.offset().cmp(&invisible_2.offset())
-                })
-                .cloned()
-                .collect::<Vec<_>>();
-
-            assert!(invisibles.is_empty(),
-                "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}");
-        }
+            .collect()
     }
 }