Compare commits

...

9 Commits

Author SHA1 Message Date
Alex Vinyals
1105495466
Merge a37f70a488 into a219d5aabb 2024-11-20 11:04:11 -08:00
Alex Vinyals
a37f70a488 bug: tab to str conversion should check the length of the tab in the palette 2024-10-14 09:11:16 +02:00
Alex Vinyals
ab57115567 Merge branch 'master' into issue-2719 2024-10-05 22:48:54 +02:00
Alexandre Vinyals Valdepeñas
970122f6d2 introduce custom style for trailing whitespace 2024-04-20 12:21:57 +02:00
Alexandre Vinyals Valdepeñas
a86a7d6920 Small enum, faster to pass by value than borrowing it. 2024-04-14 08:23:36 +02:00
Alexandre Vinyals Valdepeñas
ba893751c2 Merge branch 'master' into issue-2719
Helix is handling narrow non-breaking spaces, update
trailing whitespace tracker to take them into account
2024-04-14 00:06:10 +02:00
Alexandre Vinyals Valdepeñas
de13260bb6 feedback: stop allocating, pass render callback instead, ignore newline 2023-07-06 22:49:27 +02:00
Alexandre Vinyals Valdepeñas
99aa751c75 feedback: apply as much feedback as possible (round 1) 2023-06-24 13:36:55 +02:00
Alexandre Vinyals Valdepeñas
60c06076b2 feat(editor): add support to highlight trailing whitespace
Adds a new render configuration value `Trailing`, which can be used
to selectively enable trailing whitespace of certain whitespace characters.
2023-06-04 22:48:11 +02:00
4 changed files with 406 additions and 48 deletions

View File

@ -8,13 +8,15 @@
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)]
@ -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,
@ -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 {
@ -283,56 +287,27 @@ pub fn new(
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
@ -346,6 +321,7 @@ pub fn new(
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`.
@ -420,28 +396,61 @@ pub fn draw_grapheme(
} 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(

View File

@ -13,6 +13,7 @@
mod statusline;
mod text;
mod text_decorations;
mod trailing_whitespace;
use crate::compositor::Compositor;
use crate::filter_picker_entry;

View File

@ -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);
}
}

View File

@ -731,13 +731,88 @@ pub enum WhitespaceRender {
},
}
impl WhitespaceRender {
pub fn any(&self, value: WhitespaceRenderValue) -> bool {
self.space() == value
|| self.nbsp() == value
|| self.nnbsp() == value
|| self.tab() == value
|| self.newline() == value
}
}
pub enum WhitespaceFeature {
Regular,
Trailing,
}
impl WhitespaceFeature {
pub fn is_enabled(&self, render: WhitespaceRenderValue) -> bool {
match self {
WhitespaceFeature::Regular => matches!(render, WhitespaceRenderValue::All),
WhitespaceFeature::Trailing => matches!(
render,
WhitespaceRenderValue::All | WhitespaceRenderValue::Trailing
),
}
}
pub fn palette(self, cfg: &WhitespaceConfig, tab_width: usize) -> WhitespacePalette {
WhitespacePalette::from(self, cfg, tab_width)
}
}
#[derive(Debug)]
pub struct WhitespacePalette {
pub space: String,
pub nbsp: String,
pub nnbsp: String,
pub tab: String,
pub virtual_tab: String,
pub newline: String,
}
impl WhitespacePalette {
fn from(feature: WhitespaceFeature, cfg: &WhitespaceConfig, tab_width: usize) -> Self {
Self {
space: if feature.is_enabled(cfg.render.space()) {
cfg.characters.space.to_string()
} else {
" ".to_string()
},
nbsp: if feature.is_enabled(cfg.render.nbsp()) {
cfg.characters.nbsp.to_string()
} else {
" ".to_string()
},
nnbsp: if feature.is_enabled(cfg.render.nnbsp()) {
cfg.characters.nnbsp.to_string()
} else {
" ".to_string()
},
tab: if feature.is_enabled(cfg.render.tab()) {
cfg.characters.generate_tab(tab_width)
} else {
" ".repeat(tab_width)
},
newline: if feature.is_enabled(cfg.render.newline()) {
cfg.characters.newline.to_string()
} else {
" ".to_string()
},
virtual_tab: " ".repeat(tab_width),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WhitespaceRenderValue {
None,
All,
Trailing,
// TODO
// Selection,
All,
}
impl WhitespaceRender {
@ -862,6 +937,14 @@ fn default() -> Self {
}
}
impl WhitespaceCharacters {
pub fn generate_tab(&self, width: usize) -> String {
std::iter::once(self.tab)
.chain(std::iter::repeat(self.tabpad).take(width - 1))
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct IndentGuidesConfig {
@ -2225,3 +2308,95 @@ pub fn reset(&self) {
self.0.set(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_whitespace_render_any() {
let sut = WhitespaceRender::Basic(WhitespaceRenderValue::Trailing);
assert!(!sut.any(WhitespaceRenderValue::None));
assert!(!sut.any(WhitespaceRenderValue::All));
assert!(sut.any(WhitespaceRenderValue::Trailing));
}
#[test]
fn test_whitespace_feature_is_enabled_regular() {
let sut = WhitespaceFeature::Regular;
assert!(!sut.is_enabled(WhitespaceRenderValue::None));
assert!(!sut.is_enabled(WhitespaceRenderValue::Trailing));
assert!(sut.is_enabled(WhitespaceRenderValue::All));
}
#[test]
fn test_whitespace_feature_is_enabled_trailing() {
let sut = WhitespaceFeature::Trailing;
assert!(!sut.is_enabled(WhitespaceRenderValue::None));
assert!(sut.is_enabled(WhitespaceRenderValue::Trailing));
assert!(sut.is_enabled(WhitespaceRenderValue::All));
}
#[test]
fn test_whitespace_palette_regular_all() {
let cfg = WhitespaceConfig {
render: WhitespaceRender::Basic(WhitespaceRenderValue::All),
..Default::default()
};
let sut = WhitespacePalette::from(WhitespaceFeature::Regular, &cfg, 2);
assert_eq!("·", sut.space);
assert_eq!("", sut.nbsp);
assert_eq!("", sut.nnbsp);
assert_eq!("", sut.tab);
assert_eq!(" ", sut.virtual_tab);
assert_eq!("", sut.newline);
}
#[test]
fn test_whitespace_palette_regular_trailing() {
let cfg = WhitespaceConfig {
render: WhitespaceRender::Basic(WhitespaceRenderValue::Trailing),
..Default::default()
};
let sut = WhitespacePalette::from(WhitespaceFeature::Regular, &cfg, 2);
assert_eq!(" ", sut.space);
assert_eq!(" ", sut.nbsp);
assert_eq!(" ", sut.nnbsp);
assert_eq!(" ", sut.tab);
assert_eq!(" ", sut.virtual_tab);
assert_eq!(" ", sut.newline);
}
#[test]
fn test_whitespace_palette_trailing_all() {
let cfg = WhitespaceConfig {
render: WhitespaceRender::Basic(WhitespaceRenderValue::All),
..Default::default()
};
let sut = WhitespacePalette::from(WhitespaceFeature::Trailing, &cfg, 2);
assert_eq!("·", sut.space);
assert_eq!("", sut.nbsp);
assert_eq!("", sut.nnbsp);
assert_eq!("", sut.tab);
assert_eq!(" ", sut.virtual_tab);
assert_eq!("", sut.newline);
}
#[test]
fn test_whitespace_characters_render_tab() {
let sut = WhitespaceCharacters::default();
assert_eq!("", sut.generate_tab(1));
assert_eq!("", sut.generate_tab(2));
assert_eq!("", sut.generate_tab(3));
assert_eq!("", sut.generate_tab(4));
}
}