mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-25 02:46:17 +04:00
Merge a37f70a488
into dc941d6d24
This commit is contained in:
commit
8a7a3504e3
@ -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(
|
||||
|
@ -13,6 +13,7 @@
|
||||
mod statusline;
|
||||
mod text;
|
||||
mod text_decorations;
|
||||
mod trailing_whitespace;
|
||||
|
||||
use crate::compositor::Compositor;
|
||||
use crate::filter_picker_entry;
|
||||
|
173
helix-term/src/ui/trailing_whitespace.rs
Normal file
173
helix-term/src/ui/trailing_whitespace.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -741,13 +741,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 {
|
||||
@ -872,6 +947,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 {
|
||||
@ -2241,3 +2324,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));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user