Skip to content

Commit

Permalink
allow whitespace to be rendered
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
  • Loading branch information
2 people authored and archseer committed Apr 20, 2022
1 parent 94eba0e commit e6b865e
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 9 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,29 @@ Search specific options.
|--|--|---------|
| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
| `wrap-around`| Whether the search should wrap after depleting the matches | `true` |

### `[editor.whitespace]` Section

Options for rendering whitespace with visible characters. Use `:set whitespace.render all` to temporarily enable visible whitespace.

| 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"` |
| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space` or `newline` | See example below |

Example

```toml
[editor.whitespace]
render = "all"
# or control each character
[editor.whitespace.render]
space = "all"
tab = "all"
newline = "none"

[editor.whitespace.characters]
space = "·"
tab = ""
newline = ""
```
2 changes: 1 addition & 1 deletion book/src/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ These scopes are used for theming the editor interface.
| `ui.text` | Command prompts, popup text, etc. |
| `ui.text.focus` | |
| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
| `ui.virtual.whitespace` | Visible white-space characters |
| `ui.menu` | Code and command completion menus |
| `ui.menu.selected` | Selected autocomplete item |
| `ui.selection` | For selections in the editing area |
Expand All @@ -233,4 +234,3 @@ These scopes are used for theming the editor interface.
| `info` | Diagnostics info (gutter) |
| `hint` | Diagnostics hint (gutter) |
| `diagnostic` | For text in editing area |

1 change: 1 addition & 0 deletions helix-term/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ anyhow = "1"
once_cell = "1.10"

which = "4.2"
ropey = { version = "1.4", default-features = false }

tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
num_cpus = "1"
Expand Down
2 changes: 1 addition & 1 deletion helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ impl Application {
// the Application can apply it.
ConfigEvent::Update(editor_config) => {
let mut app_config = (*self.config.load().clone()).clone();
app_config.editor = editor_config;
app_config.editor = *editor_config;
self.config.store(Arc::new(app_config));
}
}
Expand Down
55 changes: 49 additions & 6 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,15 @@ impl EditorView {
Box::new(highlights)
};

Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights);
Self::render_text_highlights(
doc,
view.offset,
inner,
surface,
theme,
highlights,
&editor.config().whitespace,
);
Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused);
Self::render_rulers(editor, doc, view, inner, surface, theme);

Expand Down Expand Up @@ -344,7 +352,10 @@ impl EditorView {
surface: &mut Surface,
theme: &Theme,
highlights: H,
whitespace: &helix_view::editor::WhitespaceConfig,
) {
use helix_view::editor::WhitespaceRenderValue;

// It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch
// of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light).
let text = doc.text().slice(..);
Expand All @@ -353,9 +364,20 @@ impl EditorView {
let mut visual_x = 0u16;
let mut line = 0u16;
let tab_width = doc.tab_width();
let tab = " ".repeat(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 newline = if whitespace.render.newline() == WhitespaceRenderValue::All {
whitespace.characters.newline.to_string()
} else {
" ".to_string()
};

let text_style = theme.get("ui.text");
let whitespace_style = theme.get("ui.virtual.whitespace");

'outer: for event in highlights {
match event {
Expand All @@ -374,6 +396,14 @@ impl EditorView {
.iter()
.fold(text_style, |acc, span| acc.patch(theme.highlight(span.0)));

let space = if whitespace.render.space() == WhitespaceRenderValue::All
&& text.len_chars() < end
{
&space
} else {
" "
};

use helix_core::graphemes::{grapheme_width, RopeGraphemes};

for grapheme in RopeGraphemes::new(text) {
Expand All @@ -386,8 +416,8 @@ impl EditorView {
surface.set_string(
viewport.x + visual_x - offset.col as u16,
viewport.y + line,
" ",
style,
&newline,
style.patch(whitespace_style),
);
}

Expand All @@ -400,12 +430,21 @@ impl EditorView {
}
} else {
let grapheme = Cow::from(grapheme);
let is_whitespace;

let (grapheme, width) = if grapheme == "\t" {
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);
(&tab[..visual_tab_width], visual_tab_width)
let grapheme_tab_width =
ropey::str_utils::char_to_byte_idx(&tab, visual_tab_width);

(&tab[..grapheme_tab_width], visual_tab_width)
} else if grapheme == " " {
is_whitespace = true;
(space, 1)
} else {
is_whitespace = false;
// Cow will prevent allocations if span contained in a single slice
// which should really be the majority case
let width = grapheme_width(&grapheme);
Expand All @@ -418,7 +457,11 @@ impl EditorView {
viewport.x + visual_x - offset.col as u16,
viewport.y + line,
grapheme,
style,
if is_whitespace {
style.patch(whitespace_style)
} else {
style
},
);
}

Expand Down
1 change: 1 addition & 0 deletions helix-term/src/ui/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ impl<T: 'static> Component for FilePicker<T> {
surface,
&cx.editor.theme,
highlights,
&cx.editor.config().whitespace,
);

// highlight the line
Expand Down
87 changes: 86 additions & 1 deletion helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ pub struct Config {
pub lsp: LspConfig,
/// Column numbers at which to draw the rulers. Default to `[]`, meaning no rulers.
pub rulers: Vec<u16>,
#[serde(default)]
pub whitespace: WhitespaceConfig,
}

#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
Expand Down Expand Up @@ -263,6 +265,88 @@ impl std::str::FromStr for GutterType {
}
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct WhitespaceConfig {
pub render: WhitespaceRender,
pub characters: WhitespaceCharacters,
}

impl Default for WhitespaceConfig {
fn default() -> Self {
Self {
render: WhitespaceRender::Basic(WhitespaceRenderValue::None),
characters: WhitespaceCharacters::default(),
}
}
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum WhitespaceRender {
Basic(WhitespaceRenderValue),
Specific {
default: Option<WhitespaceRenderValue>,
space: Option<WhitespaceRenderValue>,
tab: Option<WhitespaceRenderValue>,
newline: Option<WhitespaceRenderValue>,
},
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WhitespaceRenderValue {
None,
// TODO
// Selection,
All,
}

impl WhitespaceRender {
pub fn space(&self) -> WhitespaceRenderValue {
match *self {
Self::Basic(val) => val,
Self::Specific { default, space, .. } => {
space.or(default).unwrap_or(WhitespaceRenderValue::None)
}
}
}
pub fn tab(&self) -> WhitespaceRenderValue {
match *self {
Self::Basic(val) => val,
Self::Specific { default, tab, .. } => {
tab.or(default).unwrap_or(WhitespaceRenderValue::None)
}
}
}
pub fn newline(&self) -> WhitespaceRenderValue {
match *self {
Self::Basic(val) => val,
Self::Specific {
default, newline, ..
} => newline.or(default).unwrap_or(WhitespaceRenderValue::None),
}
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct WhitespaceCharacters {
pub space: char,
pub tab: char,
pub newline: char,
}

impl Default for WhitespaceCharacters {
fn default() -> Self {
Self {
space: '·', // U+00B7
tab: '→', // U+2192
newline: '⏎', // U+23CE
}
}
}

impl Default for Config {
fn default() -> Self {
Self {
Expand All @@ -288,6 +372,7 @@ impl Default for Config {
search: SearchConfig::default(),
lsp: LspConfig::default(),
rulers: Vec::new(),
whitespace: WhitespaceConfig::default(),
}
}
}
Expand Down Expand Up @@ -366,7 +451,7 @@ pub struct Editor {
#[derive(Debug, Clone)]
pub enum ConfigEvent {
Refresh,
Update(Config),
Update(Box<Config>),
}

#[derive(Debug, Clone)]
Expand Down

0 comments on commit e6b865e

Please sign in to comment.