on master help sign in
allow whitespace to be rendered
e6b865eOmnikar
Cargo.lock+1
@@ -457,6 +457,7 @@ dependencies = [
457 "once_cell",
457 "once_cell",
458 "pulldown-cmark",
458 "pulldown-cmark",
459 "retain_mut",
459 "retain_mut",
460+ "ropey",
460 "serde",
461 "serde",
461 "serde_json",
462 "serde_json",
462 "signal-hook",
463 "signal-hook",
book/src/configuration.md+26
@@ -137,3 +137,29 @@ Search specific options.
137|--|--|---------|
137|--|--|---------|
138| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
138| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
139| `wrap-around`| Whether the search should wrap after depleting the matches | `true` |
139| `wrap-around`| Whether the search should wrap after depleting the matches | `true` |
140+
141+### `[editor.whitespace]` Section
142+
143+Options for rendering whitespace with visible characters. Use `:set whitespace.render all` to temporarily enable visible whitespace.
144+
145+| Key | Description | Default |
146+|-----|-------------|---------|
147+| `render` | Whether to render whitespace. May either be `"all"` or `"none"`, or a table with sub-keys `space`, `tab`, and `newline`. | `"none"` |
148+| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space` or `newline` | See example below |
149+
150+Example
151+
152+```toml
153+[editor.whitespace]
154+render = "all"
155+# or control each character
156+[editor.whitespace.render]
157+space = "all"
158+tab = "all"
159+newline = "none"
160+
161+[editor.whitespace.characters]
162+space = "·"
163+tab = "→"
164+newline = "⏎"
165+```
book/src/themes.md+1−1
@@ -224,6 +224,7 @@ These scopes are used for theming the editor interface.
224| `ui.text` | Command prompts, popup text, etc. |
224| `ui.text` | Command prompts, popup text, etc. |
225| `ui.text.focus` | |
225| `ui.text.focus` | |
226| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
226| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
227+| `ui.virtual.whitespace` | Visible white-space characters |
227| `ui.menu` | Code and command completion menus |
228| `ui.menu` | Code and command completion menus |
228| `ui.menu.selected` | Selected autocomplete item |
229| `ui.menu.selected` | Selected autocomplete item |
229| `ui.selection` | For selections in the editing area |
230| `ui.selection` | For selections in the editing area |
@@ -233,4 +234,3 @@ These scopes are used for theming the editor interface.
233| `info` | Diagnostics info (gutter) |
234| `info` | Diagnostics info (gutter) |
234| `hint` | Diagnostics hint (gutter) |
235| `hint` | Diagnostics hint (gutter) |
235| `diagnostic` | For text in editing area |
236| `diagnostic` | For text in editing area |
236−
helix-term/Cargo.toml+1
@@ -33,6 +33,7 @@ anyhow = "1"
33once_cell = "1.10"
33once_cell = "1.10"
34
34
35which = "4.2"
35which = "4.2"
36+ropey = { version = "1.4", default-features = false }
36
37
37tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
38tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
38num_cpus = "1"
39num_cpus = "1"
helix-term/src/application.rs+1−1
@@ -283,7 +283,7 @@ impl Application {
283 // the Application can apply it.
283 // the Application can apply it.
284 ConfigEvent::Update(editor_config) => {
284 ConfigEvent::Update(editor_config) => {
285 let mut app_config = (*self.config.load().clone()).clone();
285 let mut app_config = (*self.config.load().clone()).clone();
286− app_config.editor = editor_config;
286+ app_config.editor = *editor_config;
287 self.config.store(Arc::new(app_config));
287 self.config.store(Arc::new(app_config));
288 }
288 }
289 }
289 }
helix-term/src/ui/editor.rs+49−6
@@ -124,7 +124,15 @@ impl EditorView {
124 Box::new(highlights)
124 Box::new(highlights)
125 };
125 };
126
126
127− Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights);
127+ Self::render_text_highlights(
128+ doc,
129+ view.offset,
130+ inner,
131+ surface,
132+ theme,
133+ highlights,
134+ &editor.config().whitespace,
135+ );
128 Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused);
136 Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused);
129 Self::render_rulers(editor, doc, view, inner, surface, theme);
137 Self::render_rulers(editor, doc, view, inner, surface, theme);
130
138
@@ -344,7 +352,10 @@ impl EditorView {
344 surface: &mut Surface,
352 surface: &mut Surface,
345 theme: &Theme,
353 theme: &Theme,
346 highlights: H,
354 highlights: H,
355+ whitespace: &helix_view::editor::WhitespaceConfig,
347 ) {
356 ) {
357+ use helix_view::editor::WhitespaceRenderValue;
358+
348 // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch
359 // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch
349 // of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light).
360 // of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light).
350 let text = doc.text().slice(..);
361 let text = doc.text().slice(..);
@@ -353,9 +364,20 @@ impl EditorView {
353 let mut visual_x = 0u16;
364 let mut visual_x = 0u16;
354 let mut line = 0u16;
365 let mut line = 0u16;
355 let tab_width = doc.tab_width();
366 let tab_width = doc.tab_width();
367+ let tab = if whitespace.render.tab() == WhitespaceRenderValue::All {
368+ (1..tab_width).fold(whitespace.characters.tab.to_string(), |s, _| s + " ")
369+ } else {
356− let tab = " ".repeat(tab_width);
370+ " ".repeat(tab_width)
371+ };
372+ let space = whitespace.characters.space.to_string();
373+ let newline = if whitespace.render.newline() == WhitespaceRenderValue::All {
374+ whitespace.characters.newline.to_string()
375+ } else {
376+ " ".to_string()
377+ };
357
378
358 let text_style = theme.get("ui.text");
379 let text_style = theme.get("ui.text");
380+ let whitespace_style = theme.get("ui.virtual.whitespace");
359
381
360 'outer: for event in highlights {
382 'outer: for event in highlights {
361 match event {
383 match event {
@@ -374,6 +396,14 @@ impl EditorView {
374 .iter()
396 .iter()
375 .fold(text_style, |acc, span| acc.patch(theme.highlight(span.0)));
397 .fold(text_style, |acc, span| acc.patch(theme.highlight(span.0)));
376
398
399+ let space = if whitespace.render.space() == WhitespaceRenderValue::All
400+ && text.len_chars() < end
401+ {
402+ &space
403+ } else {
404+ " "
405+ };
406+
377 use helix_core::graphemes::{grapheme_width, RopeGraphemes};
407 use helix_core::graphemes::{grapheme_width, RopeGraphemes};
378
408
379 for grapheme in RopeGraphemes::new(text) {
409 for grapheme in RopeGraphemes::new(text) {
@@ -386,8 +416,8 @@ impl EditorView {
386 surface.set_string(
416 surface.set_string(
387 viewport.x + visual_x - offset.col as u16,
417 viewport.x + visual_x - offset.col as u16,
388 viewport.y + line,
418 viewport.y + line,
389− " ",
419+ &newline,
390− style,
420+ style.patch(whitespace_style),
391 );
421 );
392 }
422 }
393
423
@@ -400,12 +430,21 @@ impl EditorView {
400 }
430 }
401 } else {
431 } else {
402 let grapheme = Cow::from(grapheme);
432 let grapheme = Cow::from(grapheme);
433+ let is_whitespace;
403
434
404 let (grapheme, width) = if grapheme == "\t" {
435 let (grapheme, width) = if grapheme == "\t" {
436+ is_whitespace = true;
405 // make sure we display tab as appropriate amount of spaces
437 // make sure we display tab as appropriate amount of spaces
406 let visual_tab_width = tab_width - (visual_x as usize % tab_width);
438 let visual_tab_width = tab_width - (visual_x as usize % tab_width);
439+ let grapheme_tab_width =
440+ ropey::str_utils::char_to_byte_idx(&tab, visual_tab_width);
441+
407− (&tab[..visual_tab_width], visual_tab_width)
442+ (&tab[..grapheme_tab_width], visual_tab_width)
443+ } else if grapheme == " " {
444+ is_whitespace = true;
445+ (space, 1)
408 } else {
446 } else {
447+ is_whitespace = false;
409 // Cow will prevent allocations if span contained in a single slice
448 // Cow will prevent allocations if span contained in a single slice
410 // which should really be the majority case
449 // which should really be the majority case
411 let width = grapheme_width(&grapheme);
450 let width = grapheme_width(&grapheme);
@@ -418,7 +457,11 @@ impl EditorView {
418 viewport.x + visual_x - offset.col as u16,
457 viewport.x + visual_x - offset.col as u16,
419 viewport.y + line,
458 viewport.y + line,
420 grapheme,
459 grapheme,
421− style,
460+ if is_whitespace {
461+ style.patch(whitespace_style)
462+ } else {
463+ style
464+ },
422 );
465 );
423 }
466 }
424
467
helix-term/src/ui/picker.rs+1
@@ -240,6 +240,7 @@ impl<T: 'static> Component for FilePicker<T> {
240 surface,
240 surface,
241 &cx.editor.theme,
241 &cx.editor.theme,
242 highlights,
242 highlights,
243+ &cx.editor.config().whitespace,
243 );
244 );
244
245
245 // highlight the line
246 // highlight the line
helix-view/src/editor.rs+86−1
@@ -148,6 +148,8 @@ pub struct Config {
148 pub lsp: LspConfig,
148 pub lsp: LspConfig,
149 /// Column numbers at which to draw the rulers. Default to `[]`, meaning no rulers.
149 /// Column numbers at which to draw the rulers. Default to `[]`, meaning no rulers.
150 pub rulers: Vec<u16>,
150 pub rulers: Vec<u16>,
151+ #[serde(default)]
152+ pub whitespace: WhitespaceConfig,
151}
153}
152
154
153#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
155#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
@@ -263,6 +265,88 @@ impl std::str::FromStr for GutterType {
263 }
265 }
264}
266}
265
267
268+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
269+#[serde(default)]
270+pub struct WhitespaceConfig {
271+ pub render: WhitespaceRender,
272+ pub characters: WhitespaceCharacters,
273+}
274+
275+impl Default for WhitespaceConfig {
276+ fn default() -> Self {
277+ Self {
278+ render: WhitespaceRender::Basic(WhitespaceRenderValue::None),
279+ characters: WhitespaceCharacters::default(),
280+ }
281+ }
282+}
283+
284+#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
285+#[serde(untagged, rename_all = "kebab-case")]
286+pub enum WhitespaceRender {
287+ Basic(WhitespaceRenderValue),
288+ Specific {
289+ default: Option<WhitespaceRenderValue>,
290+ space: Option<WhitespaceRenderValue>,
291+ tab: Option<WhitespaceRenderValue>,
292+ newline: Option<WhitespaceRenderValue>,
293+ },
294+}
295+
296+#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
297+#[serde(rename_all = "kebab-case")]
298+pub enum WhitespaceRenderValue {
299+ None,
300+ // TODO
301+ // Selection,
302+ All,
303+}
304+
305+impl WhitespaceRender {
306+ pub fn space(&self) -> WhitespaceRenderValue {
307+ match *self {
308+ Self::Basic(val) => val,
309+ Self::Specific { default, space, .. } => {
310+ space.or(default).unwrap_or(WhitespaceRenderValue::None)
311+ }
312+ }
313+ }
314+ pub fn tab(&self) -> WhitespaceRenderValue {
315+ match *self {
316+ Self::Basic(val) => val,
317+ Self::Specific { default, tab, .. } => {
318+ tab.or(default).unwrap_or(WhitespaceRenderValue::None)
319+ }
320+ }
321+ }
322+ pub fn newline(&self) -> WhitespaceRenderValue {
323+ match *self {
324+ Self::Basic(val) => val,
325+ Self::Specific {
326+ default, newline, ..
327+ } => newline.or(default).unwrap_or(WhitespaceRenderValue::None),
328+ }
329+ }
330+}
331+
332+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
333+#[serde(default)]
334+pub struct WhitespaceCharacters {
335+ pub space: char,
336+ pub tab: char,
337+ pub newline: char,
338+}
339+
340+impl Default for WhitespaceCharacters {
341+ fn default() -> Self {
342+ Self {
343+ space: '·', // U+00B7
344+ tab: '→', // U+2192
345+ newline: '⏎', // U+23CE
346+ }
347+ }
348+}
349+
266impl Default for Config {
350impl Default for Config {
267 fn default() -> Self {
351 fn default() -> Self {
268 Self {
352 Self {
@@ -288,6 +372,7 @@ impl Default for Config {
288 search: SearchConfig::default(),
372 search: SearchConfig::default(),
289 lsp: LspConfig::default(),
373 lsp: LspConfig::default(),
290 rulers: Vec::new(),
374 rulers: Vec::new(),
375+ whitespace: WhitespaceConfig::default(),
291 }
376 }
292 }
377 }
293}
378}
@@ -366,7 +451,7 @@ pub struct Editor {
366#[derive(Debug, Clone)]
451#[derive(Debug, Clone)]
367pub enum ConfigEvent {
452pub enum ConfigEvent {
368 Refresh,
453 Refresh,
369− Update(Config),
454+ Update(Box<Config>),
370}
455}
371
456
372#[derive(Debug, Clone)]
457#[derive(Debug, Clone)]