From e6489e999dff64c3d75c75bf07401d01ada06ca4 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 7 May 2023 16:00:37 +0300 Subject: [PATCH] Add invisibles wrapping test --- crates/editor/src/element.rs | 213 ++++++++++++++++++++++------------- 1 file changed, 135 insertions(+), 78 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index eb3d6c2f8b850151eb099a2095cbd51533cc20d1..f71f7b7404e6bf437186bc83ab6034ade7f7d121 100644 --- a/crates/editor/src/element.rs +++ b/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>, 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::>(); + 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 { + 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::>(); - - 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::>(); - - assert!(invisibles.is_empty(), - "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}"); - } + .collect() } }