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

feat(editor): add support for trailing whitespace rendering #7215

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
103 changes: 56 additions & 47 deletions helix-term/src/ui/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ use helix_core::syntax::HighlightEvent;
use helix_core::text_annotations::TextAnnotations;
use helix_core::{visual_offset_from_block, Position, RopeSlice};
use helix_stdx::rope::RopeSliceExt;
use helix_view::editor::{WhitespaceConfig, WhitespaceRenderValue};
use helix_view::editor::WhitespaceFeature;
use helix_view::graphics::Rect;
use helix_view::theme::Style;
use helix_view::view::ViewPosition;
use helix_view::{Document, Theme};
use tui::buffer::Buffer as Surface;

use super::trailing_whitespace::{TrailingWhitespaceTracker, WhitespaceKind};

use crate::ui::text_decorations::DecorationManager;

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
Expand Down Expand Up @@ -254,6 +256,7 @@ pub struct TextRenderer<'a> {
surface: &'a mut Surface,
pub text_style: Style,
pub whitespace_style: Style,
pub trailing_whitespace_style: Style,
pub indent_guide_char: String,
pub indent_guide_style: Style,
pub newline: String,
Expand All @@ -267,6 +270,7 @@ pub struct TextRenderer<'a> {
pub draw_indent_guides: bool,
pub viewport: Rect,
pub offset: Position,
pub trailing_whitespace_tracker: TrailingWhitespaceTracker,
}

pub struct GraphemeStyle {
Expand All @@ -283,56 +287,27 @@ impl<'a> TextRenderer<'a> {
viewport: Rect,
) -> TextRenderer<'a> {
let editor_config = doc.config.load();
let WhitespaceConfig {
render: ws_render,
characters: ws_chars,
} = &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 text_style = theme.get("ui.text");

let indent_width = doc.indent_style.indent_width(tab_width) as u16;

let ws = &editor_config.whitespace;
let regular_ws = WhitespaceFeature::Regular.palette(ws, tab_width);
let trailing_ws = WhitespaceFeature::Trailing.palette(ws, tab_width);
let trailing_whitespace_tracker = TrailingWhitespaceTracker::new(ws.render, trailing_ws);

TextRenderer {
surface,
indent_guide_char: editor_config.indent_guides.character.into(),
newline,
nbsp,
nnbsp,
space,
tab,
virtual_tab,
newline: regular_ws.newline,
nbsp: regular_ws.nbsp,
nnbsp: regular_ws.nnbsp,
space: regular_ws.space,
tab: regular_ws.tab,
virtual_tab: regular_ws.virtual_tab,
whitespace_style: theme.get("ui.virtual.whitespace"),
trailing_whitespace_style: theme.get("ui.virtual.trailing_whitespace"),
indent_width,
starting_indent: offset.col / indent_width as usize
+ (offset.col % indent_width as usize != 0) as usize
Expand All @@ -346,6 +321,7 @@ impl<'a> TextRenderer<'a> {
draw_indent_guides: editor_config.indent_guides.render,
viewport,
offset,
trailing_whitespace_tracker,
}
}
/// Draws a single `grapheme` at the current render position with a specified `style`.
Expand Down Expand Up @@ -420,28 +396,61 @@ impl<'a> TextRenderer<'a> {
} else {
&self.tab
};
let mut whitespace_kind = WhitespaceKind::None;
let grapheme = match grapheme {
Grapheme::Tab { width } => {
whitespace_kind = WhitespaceKind::Tab;
let grapheme_tab_width = char_to_byte_idx(tab, width);
&tab[..grapheme_tab_width]
}
// TODO special rendering for other whitespaces?
Grapheme::Other { ref g } if g == " " => space,
Grapheme::Other { ref g } if g == "\u{00A0}" => nbsp,
Grapheme::Other { ref g } if g == "\u{202F}" => nnbsp,
Grapheme::Other { ref g } if g == " " => {
whitespace_kind = WhitespaceKind::Space;
space
}
Grapheme::Other { ref g } if g == "\u{00A0}" => {
whitespace_kind = WhitespaceKind::NonBreakingSpace;
nbsp
}
Grapheme::Other { ref g } if g == "\u{202F}" => {
whitespace_kind = WhitespaceKind::NarrowNonBreakingSpace;
nnbsp
}
Grapheme::Other { ref g } => g,
Grapheme::Newline => &self.newline,
Grapheme::Newline => {
whitespace_kind = WhitespaceKind::Newline;
&self.newline
}
};

let viewport_right_edge = self.viewport.width as usize + self.offset.col - 1;
let in_bounds = self.column_in_bounds(position.col, width);

if in_bounds {
let in_bounds_col = position.col - self.offset.col;
self.surface.set_string(
self.viewport.x + (position.col - self.offset.col) as u16,
self.viewport.x + in_bounds_col as u16,
self.viewport.y + position.row as u16,
grapheme,
style,
);

if self
.trailing_whitespace_tracker
.track(in_bounds_col, whitespace_kind)
|| position.col == viewport_right_edge
{
self.trailing_whitespace_tracker.render(
&mut |trailing_whitespace: &str, from: usize| {
self.surface.set_string(
self.viewport.x + from as u16,
self.viewport.y + position.row as u16,
trailing_whitespace,
style.patch(self.trailing_whitespace_style),
);
},
);
}
} else if cut_off_start != 0 && cut_off_start < width {
// partially on screen
let rect = Rect::new(
Expand Down
1 change: 1 addition & 0 deletions helix-term/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod spinner;
mod statusline;
mod text;
mod text_decorations;
mod trailing_whitespace;

use crate::compositor::Compositor;
use crate::filter_picker_entry;
Expand Down
173 changes: 173 additions & 0 deletions helix-term/src/ui/trailing_whitespace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
use helix_core::str_utils::char_to_byte_idx;
use helix_view::editor::{WhitespacePalette, WhitespaceRender, WhitespaceRenderValue};

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum WhitespaceKind {
None,
Space,
NonBreakingSpace,
NarrowNonBreakingSpace,
Tab,
Newline,
}

impl WhitespaceKind {
pub fn to_str(self, palette: &WhitespacePalette) -> &str {
match self {
WhitespaceKind::Space => &palette.space,
WhitespaceKind::NonBreakingSpace => &palette.nbsp,
WhitespaceKind::NarrowNonBreakingSpace => &palette.nnbsp,
WhitespaceKind::Tab => {
let grapheme_tab_width = char_to_byte_idx(&palette.tab, palette.tab.len());
&palette.tab[..grapheme_tab_width]
}
WhitespaceKind::Newline | WhitespaceKind::None => "",
}
}
}

#[derive(Debug)]
pub struct TrailingWhitespaceTracker {
enabled: bool,
palette: WhitespacePalette,
tracking_from: usize,
tracking_content: Vec<(WhitespaceKind, usize)>,
}

impl TrailingWhitespaceTracker {
pub fn new(render: WhitespaceRender, palette: WhitespacePalette) -> Self {
Self {
palette,
enabled: render.any(WhitespaceRenderValue::Trailing),
tracking_from: 0,
tracking_content: vec![],
}
}

// Tracks the whitespace and returns wether [`render`] should be called right after
// to display the trailing whitespace.
pub fn track(&mut self, from: usize, kind: WhitespaceKind) -> bool {
if !self.enabled || kind == WhitespaceKind::None {
self.tracking_content.clear();
return false;
}
if kind == WhitespaceKind::Newline {
return true;
}
if self.tracking_content.is_empty() {
self.tracking_from = from;
}
self.compress(kind);
false
}

pub fn render(&mut self, callback: &mut impl FnMut(&str, usize)) {
if self.tracking_content.is_empty() {
return;
}
let mut offset = self.tracking_from;
self.tracking_content.iter().for_each(|(kind, n)| {
let ws = kind.to_str(&self.palette).repeat(*n);
callback(&ws, offset);
offset += n;
});
self.tracking_content.clear();
}

fn compress(&mut self, kind: WhitespaceKind) {
if let Some((last_kind, n)) = self.tracking_content.last_mut() {
if *last_kind == kind {
*n += 1;
return;
}
}
self.tracking_content.push((kind, 1));
}
}

#[cfg(test)]
mod tests {

use super::*;

use helix_view::editor::WhitespaceRender;

fn palette() -> WhitespacePalette {
WhitespacePalette {
space: "S".into(),
nbsp: "N".into(),
nnbsp: "M".into(),
tab: "<TAB>".into(),
virtual_tab: "V".into(),
newline: "L".into(),
}
}

fn capture(sut: &mut TrailingWhitespaceTracker) -> (String, usize, usize) {
let mut captured_content = String::new();
let mut from: usize = 0;
let mut to: usize = 0;

sut.render(&mut |content: &str, pos: usize| {
captured_content.push_str(content);
if from == 0 {
from = pos;
}
to = pos;
});

(captured_content, from, to)
}

#[test]
fn test_trailing_whitespace_tracker_correctly_tracks_sequences() {
let ws_render = WhitespaceRender::Basic(WhitespaceRenderValue::Trailing);

let mut sut = TrailingWhitespaceTracker::new(ws_render, palette());

sut.track(5, WhitespaceKind::Space);
sut.track(6, WhitespaceKind::NonBreakingSpace);
sut.track(7, WhitespaceKind::NarrowNonBreakingSpace);
sut.track(8, WhitespaceKind::Tab);

let (content, from, to) = capture(&mut sut);

assert_eq!(5, from);
assert_eq!(8, to);
assert_eq!("SNM<TAB>", content);

// Now we break the sequence
sut.track(6, WhitespaceKind::None);

let (content, from, to) = capture(&mut sut);
assert_eq!(0, from);
assert_eq!(0, to);
assert_eq!("", content);

sut.track(10, WhitespaceKind::Tab);
sut.track(11, WhitespaceKind::NonBreakingSpace);
sut.track(12, WhitespaceKind::NarrowNonBreakingSpace);
sut.track(13, WhitespaceKind::Space);

let (content, from, to) = capture(&mut sut);
assert_eq!(10, from);
assert_eq!(13, to);
assert_eq!("<TAB>NMS", content);

// Verify compression works
sut.track(20, WhitespaceKind::Space);
sut.track(21, WhitespaceKind::Space);
sut.track(22, WhitespaceKind::NonBreakingSpace);
sut.track(23, WhitespaceKind::NonBreakingSpace);
sut.track(24, WhitespaceKind::NarrowNonBreakingSpace);
sut.track(25, WhitespaceKind::NarrowNonBreakingSpace);
sut.track(26, WhitespaceKind::Tab);
sut.track(27, WhitespaceKind::Tab);
sut.track(28, WhitespaceKind::Tab);

let (content, from, to) = capture(&mut sut);
assert_eq!(20, from);
assert_eq!(26, to); // Compression means last tracked token is on 26 instead of 28
assert_eq!("SSNNMM<TAB><TAB><TAB>", content);
}
}
Loading