Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visible whitespace only for selections #2208

Closed
2 changes: 1 addition & 1 deletion book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 59 additions & 18 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<H: Iterator<Item = HighlightEvent>>(
doc: &Document,
offset: Position,
Expand All @@ -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(" ", &nbsp_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");
Expand Down Expand Up @@ -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
Expand All @@ -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
{
&nbsp
if in_selection {
&nbsp_selected
} else {
&nbsp_unselected
}
} else {
" "
};
Expand All @@ -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),
);
}
Expand All @@ -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 == " " {
Expand Down
3 changes: 1 addition & 2 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,7 @@ pub enum WhitespaceRender {
#[serde(rename_all = "kebab-case")]
pub enum WhitespaceRenderValue {
None,
// TODO
// Selection,
Selection,
All,
}

Expand Down
14 changes: 14 additions & 0 deletions helix-view/src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ pub struct Theme {
styles: HashMap<String, Style>,
// tree-sitter highlight styles are stored in a Vec to optimize lookups
scopes: Vec<String>,
selection_scopes: Vec<usize>,
highlights: Vec<Style>,
}

Expand All @@ -112,6 +113,7 @@ impl<'de> Deserialize<'de> for Theme {
{
let mut styles = HashMap::new();
let mut scopes = Vec::new();
let mut selection_scopes = Vec::new();
let mut highlights = Vec::new();

if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
Expand All @@ -138,13 +140,20 @@ impl<'de> Deserialize<'de> for Theme {

// these are used both as UI and as highlights
styles.insert(name.clone(), style);
if name.starts_with("ui.selection")
|| name == "ui.cursor"
|| name.starts_with("ui.cursor.")
{
selection_scopes.push(scopes.len());
}
scopes.push(name);
highlights.push(style);
}
}

Ok(Self {
scopes,
selection_scopes,
styles,
highlights,
})
Expand Down Expand Up @@ -174,6 +183,11 @@ impl Theme {
&self.scopes
}

#[inline]
pub fn selection_scopes(&self) -> &[usize] {
&self.selection_scopes
}

pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
self.scopes().iter().position(|s| s == scope)
}
Expand Down