From 580d9d28f7d20d6a03754f363f210e0017c93dae Mon Sep 17 00:00:00 2001 From: sunshine <24392379+armonjam@users.noreply.github.com> Date: Sat, 17 Aug 2024 21:42:05 -0400 Subject: [PATCH] feat: render selected whitespace only --- helix-term/src/ui/document.rs | 180 +++++++++++++++++++++++----------- helix-term/src/ui/editor.rs | 6 ++ helix-term/src/ui/picker.rs | 1 + helix-view/src/editor.rs | 3 +- 4 files changed, 133 insertions(+), 57 deletions(-) diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index 79145ba04..218cfbaeb 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -6,9 +6,11 @@ use helix_core::str_utils::char_to_byte_idx; use helix_core::syntax::Highlight; use helix_core::syntax::HighlightEvent; use helix_core::text_annotations::TextAnnotations; -use helix_core::{visual_offset_from_block, Position, RopeSlice}; +use helix_core::{visual_offset_from_block, Position, RopeSlice, Selection}; use helix_stdx::rope::RopeSliceExt; -use helix_view::editor::{WhitespaceConfig, WhitespaceRenderValue}; +use helix_view::editor::{ + WhitespaceCharacters, WhitespaceConfig, WhitespaceRender, WhitespaceRenderValue, +}; use helix_view::graphics::Rect; use helix_view::theme::Style; use helix_view::view::ViewPosition; @@ -86,6 +88,7 @@ pub fn render_document( surface: &mut Surface, viewport: Rect, doc: &Document, + selection: Option<&Selection>, offset: ViewPosition, doc_annotations: &TextAnnotations, syntax_highlight_iter: impl Iterator, @@ -103,6 +106,7 @@ pub fn render_document( render_text( &mut renderer, doc.text().slice(..), + selection, offset.anchor, &doc.text_format(viewport.width, Some(theme)), doc_annotations, @@ -117,6 +121,7 @@ pub fn render_document( pub fn render_text( renderer: &mut TextRenderer, text: RopeSlice<'_>, + selection: Option<&Selection>, anchor: usize, text_fmt: &TextFormat, text_annotations: &TextAnnotations, @@ -234,10 +239,16 @@ pub fn render_text( decorations.decorate_grapheme(renderer, &grapheme); let virt = grapheme.is_virtual(); + let is_selected = selection.is_some_and(|selection| { + selection + .iter() + .any(|range| range.contains(grapheme.char_idx)) + }); let grapheme_width = renderer.draw_grapheme( grapheme.raw, grapheme_style, virt, + is_selected, &mut last_line_indent_level, &mut is_in_indent_area, grapheme.visual_pos, @@ -252,16 +263,11 @@ pub fn render_text( #[derive(Debug)] pub struct TextRenderer<'a> { surface: &'a mut Surface, + whitespace_entries: WhitespaceEntries, pub text_style: Style, pub whitespace_style: Style, pub indent_guide_char: String, pub indent_guide_style: Style, - pub newline: String, - pub nbsp: String, - pub nnbsp: String, - pub space: String, - pub tab: String, - pub virtual_tab: String, pub indent_width: u16, pub starting_indent: usize, pub draw_indent_guides: bool, @@ -289,36 +295,7 @@ impl<'a> TextRenderer<'a> { } = &editor_config.whitespace; let tab_width = doc.tab_width(); - let tab = if ws_render.tab() == WhitespaceRenderValue::All { - std::iter::once(ws_chars.tab) - .chain(std::iter::repeat(ws_chars.tabpad).take(tab_width - 1)) - .collect() - } else { - " ".repeat(tab_width) - }; - let virtual_tab = " ".repeat(tab_width); - let newline = if ws_render.newline() == WhitespaceRenderValue::All { - ws_chars.newline.into() - } else { - " ".to_owned() - }; - - let space = if ws_render.space() == WhitespaceRenderValue::All { - ws_chars.space.into() - } else { - " ".to_owned() - }; - let nbsp = if ws_render.nbsp() == WhitespaceRenderValue::All { - ws_chars.nbsp.into() - } else { - " ".to_owned() - }; - let nnbsp = if ws_render.nnbsp() == WhitespaceRenderValue::All { - ws_chars.nnbsp.into() - } else { - " ".to_owned() - }; - + let whitespace_entries = WhitespaceEntries::new(ws_render, ws_chars, tab_width); let text_style = theme.get("ui.text"); let indent_width = doc.indent_style.indent_width(tab_width) as u16; @@ -326,12 +303,7 @@ impl<'a> TextRenderer<'a> { TextRenderer { surface, indent_guide_char: editor_config.indent_guides.character.into(), - newline, - nbsp, - nnbsp, - space, - tab, - virtual_tab, + whitespace_entries, whitespace_style: theme.get("ui.virtual.whitespace"), indent_width, starting_indent: offset.col / indent_width as usize @@ -368,10 +340,11 @@ impl<'a> TextRenderer<'a> { style = style.patch(self.whitespace_style); } + let virtual_tab = &self.whitespace_entries.tab.render(true, false); let grapheme = match grapheme { Grapheme::Tab { width } => { - let grapheme_tab_width = char_to_byte_idx(&self.virtual_tab, width); - &self.virtual_tab[..grapheme_tab_width] + let grapheme_tab_width = char_to_byte_idx(virtual_tab, width); + &virtual_tab[..grapheme_tab_width] } Grapheme::Other { ref g } if g == "\u{00A0}" => " ", Grapheme::Other { ref g } => g, @@ -393,6 +366,7 @@ impl<'a> TextRenderer<'a> { grapheme: Grapheme, grapheme_style: GraphemeStyle, is_virtual: bool, + is_selected: bool, last_indent_level: &mut usize, is_in_indent_area: &mut bool, mut position: Position, @@ -412,14 +386,14 @@ impl<'a> TextRenderer<'a> { style = style.patch(grapheme_style.overlay_style); let width = grapheme.width(); - let space = if is_virtual { " " } else { &self.space }; - let nbsp = if is_virtual { " " } else { &self.nbsp }; - let nnbsp = if is_virtual { " " } else { &self.nnbsp }; - let tab = if is_virtual { - &self.virtual_tab - } else { - &self.tab - }; + + let ws = &self.whitespace_entries; + let tab = &ws.tab.render(is_virtual, is_selected); + let space = &ws.space.render(is_virtual, is_selected); + let nbsp = &ws.nbsp.render(is_virtual, is_selected); + let nnbsp = &ws.nnbsp.render(is_virtual, is_selected); + let newline = &ws.newline.render(false, is_selected); + let grapheme = match grapheme { Grapheme::Tab { width } => { let grapheme_tab_width = char_to_byte_idx(tab, width); @@ -430,7 +404,7 @@ impl<'a> TextRenderer<'a> { Grapheme::Other { ref g } if g == "\u{00A0}" => nbsp, Grapheme::Other { ref g } if g == "\u{202F}" => nnbsp, Grapheme::Other { ref g } => g, - Grapheme::Newline => &self.newline, + Grapheme::Newline => newline, }; let in_bounds = self.column_in_bounds(position.col + width - 1); @@ -550,3 +524,99 @@ impl<'a> TextRenderer<'a> { ) } } + +#[derive(Debug)] +struct WhitespacePadding { + grapheme_width: usize, + padding_character: char, +} + +#[derive(Debug)] +struct Whitespace { + render_value: WhitespaceRenderValue, + character: char, + padding: Option, +} + +impl Whitespace { + fn render_hidden(&self) -> String { + let target_width = self.padding.as_ref().map(|p| p.grapheme_width).unwrap_or(1); + " ".repeat(target_width) + } + fn render_visible(&self) -> String { + match self.padding { + Some(WhitespacePadding { + grapheme_width, + padding_character, + }) => std::iter::once(self.character) + .chain(std::iter::repeat(padding_character).take(grapheme_width - 1)) + .collect(), + None => self.character.to_string(), + } + } + fn is_visible(&self, is_virtual: bool, is_selected: bool) -> bool { + if is_virtual { + return false; + } + match self.render_value { + WhitespaceRenderValue::All => true, + WhitespaceRenderValue::Selection => is_selected, + WhitespaceRenderValue::None => false, + } + } + fn render(&self, is_virtual: bool, is_selected: bool) -> String { + if self.is_visible(is_virtual, is_selected) { + self.render_visible() + } else { + self.render_hidden() + } + } +} + +#[derive(Debug)] +struct WhitespaceEntries { + space: Whitespace, + nbsp: Whitespace, + nnbsp: Whitespace, + tab: Whitespace, + newline: Whitespace, +} + +impl WhitespaceEntries { + fn new( + whitespace_render: &WhitespaceRender, + whitespace_characters: &WhitespaceCharacters, + tab_width: usize, + ) -> Self { + WhitespaceEntries { + space: Whitespace { + render_value: whitespace_render.space(), + character: whitespace_characters.space, + padding: None, + }, + nbsp: Whitespace { + render_value: whitespace_render.nbsp(), + character: whitespace_characters.nbsp, + padding: None, + }, + nnbsp: Whitespace { + render_value: whitespace_render.nnbsp(), + character: whitespace_characters.nnbsp, + padding: None, + }, + tab: Whitespace { + render_value: whitespace_render.tab(), + character: whitespace_characters.tab, + padding: Some(WhitespacePadding { + grapheme_width: tab_width, + padding_character: whitespace_characters.tabpad, + }), + }, + newline: Whitespace { + render_value: whitespace_render.newline(), + character: whitespace_characters.newline, + padding: None, + }, + } + } +} diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index f7541fe25..d85f85172 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -201,10 +201,16 @@ impl EditorView { inline_diagnostic_config, config.end_of_line_diagnostics, )); + let selection = if is_focused { + Some(doc.selection(view.id)) + } else { + None + }; render_document( surface, inner, doc, + selection, view_offset, &text_annotations, syntax_highlights, diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 82fe96891..21aed8e08 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -935,6 +935,7 @@ impl Picker { surface, inner, doc, + None, offset, // TODO: compute text annotations asynchronously here (like inlay hints) &TextAnnotations::default(), diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 1708b3b4e..a9ee5c5ca 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -732,8 +732,7 @@ pub enum WhitespaceRender { #[serde(rename_all = "kebab-case")] pub enum WhitespaceRenderValue { None, - // TODO - // Selection, + Selection, All, }