mirror of
https://github.com/ilyakooo0/helix.git
synced 2024-11-29 05:27:07 +03:00
allow whitespace to be rendered
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
This commit is contained in:
parent
94eba0e66a
commit
e6b865ed0b
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -457,6 +457,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"retain_mut",
|
"retain_mut",
|
||||||
|
"ropey",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
|
@ -137,3 +137,29 @@ Search specific options.
|
|||||||
|--|--|---------|
|
|--|--|---------|
|
||||||
| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
|
| `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` |
|
| `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 = "⏎"
|
||||||
|
```
|
||||||
|
@ -224,6 +224,7 @@ These scopes are used for theming the editor interface.
|
|||||||
| `ui.text` | Command prompts, popup text, etc. |
|
| `ui.text` | Command prompts, popup text, etc. |
|
||||||
| `ui.text.focus` | |
|
| `ui.text.focus` | |
|
||||||
| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
|
| `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` | Code and command completion menus |
|
||||||
| `ui.menu.selected` | Selected autocomplete item |
|
| `ui.menu.selected` | Selected autocomplete item |
|
||||||
| `ui.selection` | For selections in the editing area |
|
| `ui.selection` | For selections in the editing area |
|
||||||
@ -233,4 +234,3 @@ These scopes are used for theming the editor interface.
|
|||||||
| `info` | Diagnostics info (gutter) |
|
| `info` | Diagnostics info (gutter) |
|
||||||
| `hint` | Diagnostics hint (gutter) |
|
| `hint` | Diagnostics hint (gutter) |
|
||||||
| `diagnostic` | For text in editing area |
|
| `diagnostic` | For text in editing area |
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ anyhow = "1"
|
|||||||
once_cell = "1.10"
|
once_cell = "1.10"
|
||||||
|
|
||||||
which = "4.2"
|
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"] }
|
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
|
||||||
num_cpus = "1"
|
num_cpus = "1"
|
||||||
|
@ -283,7 +283,7 @@ impl Application {
|
|||||||
// the Application can apply it.
|
// the Application can apply it.
|
||||||
ConfigEvent::Update(editor_config) => {
|
ConfigEvent::Update(editor_config) => {
|
||||||
let mut app_config = (*self.config.load().clone()).clone();
|
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));
|
self.config.store(Arc::new(app_config));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,15 @@ impl EditorView {
|
|||||||
Box::new(highlights)
|
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_gutter(editor, doc, view, view.area, surface, theme, is_focused);
|
||||||
Self::render_rulers(editor, doc, view, inner, surface, theme);
|
Self::render_rulers(editor, doc, view, inner, surface, theme);
|
||||||
|
|
||||||
@ -344,7 +352,10 @@ impl EditorView {
|
|||||||
surface: &mut Surface,
|
surface: &mut Surface,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
highlights: H,
|
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
|
// 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).
|
// of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light).
|
||||||
let text = doc.text().slice(..);
|
let text = doc.text().slice(..);
|
||||||
@ -353,9 +364,20 @@ impl EditorView {
|
|||||||
let mut visual_x = 0u16;
|
let mut visual_x = 0u16;
|
||||||
let mut line = 0u16;
|
let mut line = 0u16;
|
||||||
let tab_width = doc.tab_width();
|
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 text_style = theme.get("ui.text");
|
||||||
|
let whitespace_style = theme.get("ui.virtual.whitespace");
|
||||||
|
|
||||||
'outer: for event in highlights {
|
'outer: for event in highlights {
|
||||||
match event {
|
match event {
|
||||||
@ -374,6 +396,14 @@ impl EditorView {
|
|||||||
.iter()
|
.iter()
|
||||||
.fold(text_style, |acc, span| acc.patch(theme.highlight(span.0)));
|
.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};
|
use helix_core::graphemes::{grapheme_width, RopeGraphemes};
|
||||||
|
|
||||||
for grapheme in RopeGraphemes::new(text) {
|
for grapheme in RopeGraphemes::new(text) {
|
||||||
@ -386,8 +416,8 @@ impl EditorView {
|
|||||||
surface.set_string(
|
surface.set_string(
|
||||||
viewport.x + visual_x - offset.col as u16,
|
viewport.x + visual_x - offset.col as u16,
|
||||||
viewport.y + line,
|
viewport.y + line,
|
||||||
" ",
|
&newline,
|
||||||
style,
|
style.patch(whitespace_style),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,12 +430,21 @@ impl EditorView {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let grapheme = Cow::from(grapheme);
|
let grapheme = Cow::from(grapheme);
|
||||||
|
let is_whitespace;
|
||||||
|
|
||||||
let (grapheme, width) = if grapheme == "\t" {
|
let (grapheme, width) = if grapheme == "\t" {
|
||||||
|
is_whitespace = true;
|
||||||
// make sure we display tab as appropriate amount of spaces
|
// make sure we display tab as appropriate amount of spaces
|
||||||
let visual_tab_width = tab_width - (visual_x as usize % tab_width);
|
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 {
|
} else {
|
||||||
|
is_whitespace = false;
|
||||||
// Cow will prevent allocations if span contained in a single slice
|
// Cow will prevent allocations if span contained in a single slice
|
||||||
// which should really be the majority case
|
// which should really be the majority case
|
||||||
let width = grapheme_width(&grapheme);
|
let width = grapheme_width(&grapheme);
|
||||||
@ -418,7 +457,11 @@ impl EditorView {
|
|||||||
viewport.x + visual_x - offset.col as u16,
|
viewport.x + visual_x - offset.col as u16,
|
||||||
viewport.y + line,
|
viewport.y + line,
|
||||||
grapheme,
|
grapheme,
|
||||||
style,
|
if is_whitespace {
|
||||||
|
style.patch(whitespace_style)
|
||||||
|
} else {
|
||||||
|
style
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,6 +240,7 @@ impl<T: 'static> Component for FilePicker<T> {
|
|||||||
surface,
|
surface,
|
||||||
&cx.editor.theme,
|
&cx.editor.theme,
|
||||||
highlights,
|
highlights,
|
||||||
|
&cx.editor.config().whitespace,
|
||||||
);
|
);
|
||||||
|
|
||||||
// highlight the line
|
// highlight the line
|
||||||
|
@ -148,6 +148,8 @@ pub struct Config {
|
|||||||
pub lsp: LspConfig,
|
pub lsp: LspConfig,
|
||||||
/// Column numbers at which to draw the rulers. Default to `[]`, meaning no rulers.
|
/// Column numbers at which to draw the rulers. Default to `[]`, meaning no rulers.
|
||||||
pub rulers: Vec<u16>,
|
pub rulers: Vec<u16>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub whitespace: WhitespaceConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
@ -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 {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -288,6 +372,7 @@ impl Default for Config {
|
|||||||
search: SearchConfig::default(),
|
search: SearchConfig::default(),
|
||||||
lsp: LspConfig::default(),
|
lsp: LspConfig::default(),
|
||||||
rulers: Vec::new(),
|
rulers: Vec::new(),
|
||||||
|
whitespace: WhitespaceConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -366,7 +451,7 @@ pub struct Editor {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ConfigEvent {
|
pub enum ConfigEvent {
|
||||||
Refresh,
|
Refresh,
|
||||||
Update(Config),
|
Update(Box<Config>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
Loading…
Reference in New Issue
Block a user