diff --git a/book/src/configuration.md b/book/src/configuration.md index e418869fc223..5ea1e430baa4 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -189,7 +189,7 @@ Options for rendering whitespace with visible characters. Use `:set whitespace.r | Key | Description | Default | |-----|-------------|---------| -| `render` | Whether to render whitespace. May either be `"all"` or `"none"`, or a table with sub-keys `space`, `tab`, and `newline`. | `"none"` | +| `render` | Whether to render whitespace. May either be `"all"`, `"selection"`, or `"none"`, or a table with sub-keys `space`, `tab`, and `newline`. | `"none"` | | `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp` or `newline` | See example below | Example diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 6b3163748fa2..c335bc8bd879 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -375,6 +375,19 @@ impl EditorView { spans } + fn render_whitespace<'a>( + hidden: &'a str, + visible: &'a str, + pref: helix_view::editor::WhitespaceRenderValue, + ) -> (&'a str, &'a str) { + use helix_view::editor::WhitespaceRenderValue; + match pref { + WhitespaceRenderValue::None => (hidden, hidden), + WhitespaceRenderValue::All => (visible, visible), + WhitespaceRenderValue::Selection => (visible, hidden), + } + } + pub fn render_text_highlights>( doc: &Document, offset: Position, @@ -395,19 +408,21 @@ impl EditorView { let mut visual_x = 0u16; let mut line = 0u16; let tab_width = doc.tab_width(); - let tab = if whitespace.render.tab() == WhitespaceRenderValue::All { - (1..tab_width).fold(whitespace.characters.tab.to_string(), |s, _| s + " ") - } else { - " ".repeat(tab_width) - }; - let space = whitespace.characters.space.to_string(); - let nbsp = whitespace.characters.nbsp.to_string(); - let newline = if whitespace.render.newline() == WhitespaceRenderValue::All { - whitespace.characters.newline.to_string() - } else { - " ".to_string() - }; + let tab_hidden = " ".repeat(tab_width); + let tab_visible = + (1..tab_width).fold(whitespace.characters.tab.to_string(), |s, _| s + " "); + let space_visible = whitespace.characters.space.to_string(); + let nbsp_visible = whitespace.characters.nbsp.to_string(); + let newline_visible = whitespace.characters.newline.to_string(); let indent_guide_char = config.indent_guides.character.to_string(); + let (tab_selected, tab_unselected) = + Self::render_whitespace(&tab_hidden, &tab_visible, whitespace.render.tab()); + let (space_selected, space_unselected) = + Self::render_whitespace(" ", &space_visible, whitespace.render.space()); + let (nbsp_selected, nbsp_unselected) = + Self::render_whitespace(" ",  _visible, whitespace.render.nbsp()); + let (newline_selected, newline_unselected) = + Self::render_whitespace(" ", &newline_visible, whitespace.render.newline()); let text_style = theme.get("ui.text"); let whitespace_style = theme.get("ui.virtual.whitespace"); @@ -441,16 +456,25 @@ impl EditorView { } }; + let mut selection_scope_depth = 0usize; + 'outer: for event in highlights { match event { HighlightEvent::HighlightStart(span) => { spans.push(span); + + let in_selection = selection_scope_depth > 0; + if in_selection || theme.selection_scopes().contains(&span.0) { + selection_scope_depth += 1; + } } HighlightEvent::HighlightEnd => { spans.pop(); + selection_scope_depth = selection_scope_depth.saturating_sub(1); } HighlightEvent::Source { start, end } => { let is_trailing_cursor = text.len_chars() < end; + let in_selection = selection_scope_depth > 0; // `unwrap_or_else` part is for off-the-end indices of // the rope, to allow cursor highlighting at the end @@ -460,18 +484,26 @@ impl EditorView { .iter() .fold(text_style, |acc, span| acc.patch(theme.highlight(span.0))); - let space = if whitespace.render.space() == WhitespaceRenderValue::All + let space = if whitespace.render.space() != WhitespaceRenderValue::None && !is_trailing_cursor { - &space + if in_selection { + &space_selected + } else { + &space_unselected + } } else { " " }; - let nbsp = if whitespace.render.nbsp() == WhitespaceRenderValue::All + let nbsp = if whitespace.render.nbsp() != WhitespaceRenderValue::None && text.len_chars() < end { -   + if in_selection { +  _selected + } else { +  _unselected + } } else { " " }; @@ -488,7 +520,11 @@ impl EditorView { surface.set_string( viewport.x + visual_x - offset.col as u16, viewport.y + line, - &newline, + if in_selection { + &newline_selected + } else { + &newline_unselected + }, style.patch(whitespace_style), ); } @@ -511,8 +547,13 @@ impl EditorView { is_whitespace = true; // make sure we display tab as appropriate amount of spaces let visual_tab_width = tab_width - (visual_x as usize % tab_width); + let tab = if in_selection { + &tab_selected + } else { + &tab_unselected + }; let grapheme_tab_width = - helix_core::str_utils::char_to_byte_idx(&tab, visual_tab_width); + helix_core::str_utils::char_to_byte_idx(tab, visual_tab_width); (&tab[..grapheme_tab_width], visual_tab_width) } else if grapheme == " " { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 1e7f508c14bf..e831ec65071e 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -392,8 +392,7 @@ pub enum WhitespaceRender { #[serde(rename_all = "kebab-case")] pub enum WhitespaceRenderValue { None, - // TODO - // Selection, + Selection, All, } diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index fa5fa702804b..eced2d01bc91 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -102,6 +102,7 @@ pub struct Theme { styles: HashMap, // tree-sitter highlight styles are stored in a Vec to optimize lookups scopes: Vec, + selection_scopes: Vec, highlights: Vec