diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 87eab4d068597942ac0a4e3f50f1b7555dd21272..873d031393c0c888a0943d181e24f0ffbe291e7b 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4071,6 +4071,7 @@ impl LineWithInvisibles { if non_whitespace_added || !inside_wrapped_string { invisibles.push(Invisible::Tab { line_start_offset: line.len(), + line_end_offset: line.len() + line_chunk.len(), }); } } else { @@ -4186,16 +4187,15 @@ impl LineWithInvisibles { whitespace_setting: ShowWhitespaceSetting, cx: &mut WindowContext, ) { - let allowed_invisibles_regions = match whitespace_setting { - ShowWhitespaceSetting::None => return, - ShowWhitespaceSetting::Selection => Some(selection_ranges), - ShowWhitespaceSetting::All => None, - }; - - for invisible in &self.invisibles { - let (&token_offset, invisible_symbol) = match invisible { - Invisible::Tab { line_start_offset } => (line_start_offset, &layout.tab_invisible), - Invisible::Whitespace { line_offset } => (line_offset, &layout.space_invisible), + let extract_whitespace_info = |invisible: &Invisible| { + let (token_offset, token_end_offset, invisible_symbol) = match invisible { + Invisible::Tab { + line_start_offset, + line_end_offset, + } => (*line_start_offset, *line_end_offset, &layout.tab_invisible), + Invisible::Whitespace { line_offset } => { + (*line_offset, line_offset + 1, &layout.space_invisible) + } }; let x_offset = self.x_for_index(token_offset); @@ -4207,17 +4207,73 @@ impl LineWithInvisibles { line_y, ); - if let Some(allowed_regions) = allowed_invisibles_regions { - let invisible_point = DisplayPoint::new(row, token_offset as u32); - if !allowed_regions + ( + [token_offset, token_end_offset], + Box::new(move |cx: &mut WindowContext| { + invisible_symbol.paint(origin, line_height, cx).log_err(); + }), + ) + }; + + let invisible_iter = self.invisibles.iter().map(extract_whitespace_info); + match whitespace_setting { + ShowWhitespaceSetting::None => return, + ShowWhitespaceSetting::All => invisible_iter.for_each(|(_, paint)| paint(cx)), + ShowWhitespaceSetting::Selection => invisible_iter.for_each(|([start, _], paint)| { + let invisible_point = DisplayPoint::new(row, start as u32); + if !selection_ranges .iter() .any(|region| region.start <= invisible_point && invisible_point < region.end) { - continue; + return; + } + + paint(cx); + }), + + // For a whitespace to be on a boundary, any of the following conditions need to be met: + // - It is a tab + // - It is adjacent to an edge (start or end) + // - It is adjacent to a whitespace (left or right) + ShowWhitespaceSetting::Boundary => { + // We'll need to keep track of the last invisible we've seen and then check if we are adjacent to it for some of + // the above cases. + // Note: We zip in the original `invisibles` to check for tab equality + let mut last_seen: Option<(bool, usize, Box)> = None; + for (([start, end], paint), invisible) in + invisible_iter.zip_eq(self.invisibles.iter()) + { + let should_render = match (&last_seen, invisible) { + (_, Invisible::Tab { .. }) => true, + (Some((_, last_end, _)), _) => *last_end == start, + _ => false, + }; + + if should_render || start == 0 || end == self.len { + paint(cx); + + // Since we are scanning from the left, we will skip over the first available whitespace that is part + // of a boundary between non-whitespace segments, so we correct by manually redrawing it if needed. + if let Some((should_render_last, last_end, paint_last)) = last_seen { + // Note that we need to make sure that the last one is actually adjacent + if !should_render_last && last_end == start { + paint_last(cx); + } + } + } + + // Manually render anything within a selection + let invisible_point = DisplayPoint::new(row, start as u32); + if selection_ranges.iter().any(|region| { + region.start <= invisible_point && invisible_point < region.end + }) { + paint(cx); + } + + last_seen = Some((should_render, end, paint)); } } - invisible_symbol.paint(origin, line_height, cx).log_err(); - } + }; } pub fn x_for_index(&self, index: usize) -> Pixels { @@ -4307,8 +4363,18 @@ impl LineWithInvisibles { #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Invisible { - Tab { line_start_offset: usize }, - Whitespace { line_offset: usize }, + /// A tab character + /// + /// A tab character is internally represented by spaces (configured by the user's tab width) + /// aligned to the nearest column, so it's necessary to store the start and end offset for + /// adjacency checks. + Tab { + line_start_offset: usize, + line_end_offset: usize, + }, + Whitespace { + line_offset: usize, + }, } impl EditorElement { @@ -5853,15 +5919,18 @@ mod tests { let expected_invisibles = vec![ Invisible::Tab { line_start_offset: 0, + line_end_offset: TAB_SIZE as usize, }, Invisible::Whitespace { line_offset: TAB_SIZE as usize, }, Invisible::Tab { line_start_offset: TAB_SIZE as usize + 1, + line_end_offset: TAB_SIZE as usize * 2, }, Invisible::Tab { line_start_offset: TAB_SIZE as usize * 2 + 1, + line_end_offset: TAB_SIZE as usize * 3, }, Invisible::Whitespace { line_offset: TAB_SIZE as usize * 3 + 1, @@ -5915,10 +5984,11 @@ mod tests { #[gpui::test] fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) { let tab_size = 4; - let input_text = "a\tbcd ".repeat(9); + let input_text = "a\tbcd ".repeat(9); let repeated_invisibles = [ Invisible::Tab { line_start_offset: 1, + line_end_offset: tab_size as usize, }, Invisible::Whitespace { line_offset: tab_size as usize + 3, @@ -5929,6 +5999,12 @@ mod tests { Invisible::Whitespace { line_offset: tab_size as usize + 5, }, + Invisible::Whitespace { + line_offset: tab_size as usize + 6, + }, + Invisible::Whitespace { + line_offset: tab_size as usize + 7, + }, ]; let expected_invisibles = std::iter::once(repeated_invisibles) .cycle() diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 9b9415ac8a24af91ffbc4129f505cae22c9a7767..4fb7b038800bd5c97a6a8fc5b4a21c65f97c2923 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -391,6 +391,13 @@ pub enum ShowWhitespaceSetting { None, /// Draw all invisible symbols. All, + /// Draw whitespace only at boundaries. + /// + /// For a whitespace to be on a boundary, any of the following conditions need to be met: + /// - It is a tab + /// - It is adjacent to an edge (start or end) + /// - It is adjacent to a whitespace (left or right) + Boundary, } /// Controls which formatter should be used when formatting code.