Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rainbow indentation guides #4493

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,12 @@ tabpad = "·" # Tabs will look like "→···" (depending on tab width)

Options for rendering vertical indent guides.

| Key | Description | Default |
| --- | --- | --- |
| `render` | Whether to render indent guides | `false` |
| `character` | Literal character to use for rendering the indent guide | `│` |
| `skip-levels` | Number of indent levels to skip | `0` |
| Key | Description | Default |
| --- | --- | --- |
| `render` | Whether to render indent guides. | `false` |
| `character` | Literal character to use for rendering the indent guide | `│` |
| `skip-levels` | Number of indent levels to skip | `0` |
| `rainbow-option` | Enum to set rainbow indentations. Options: `normal`, `dim` and `none`| `none` |

Example:

Expand All @@ -269,6 +270,7 @@ Example:
render = true
character = "╎" # Some characters that work well: "▏", "┆", "┊", "⸽"
skip-levels = 1
rainbow-option = "normal"
```

### `[editor.gutters]` Section
Expand Down
11 changes: 11 additions & 0 deletions book/src/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,17 @@ inherits = "boo_berry"
berry = "#2A2A4D"
```

### Rainbow

The `rainbow` key is used for rainbow highlight for matching brackets.
The key is a list of styles.

```toml
rainbow = ["#ff0000", "#ffa500", "#fff000", { fg = "#00ff00", modifiers = ["bold"] }]
```

Colors from the palette and modifiers may be used.

### Scopes

The following is a list of scopes available to use for styling:
Expand Down
42 changes: 32 additions & 10 deletions helix-term/src/ui/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ use helix_core::syntax::Highlight;
use helix_core::syntax::HighlightEvent;
use helix_core::text_annotations::TextAnnotations;
use helix_core::{visual_offset_from_block, Position, RopeSlice};
use helix_view::editor::{WhitespaceConfig, WhitespaceRenderValue};
use helix_view::editor::{RainbowIndentOptions, WhitespaceConfig, WhitespaceRenderValue};
use helix_view::graphics::Rect;
use helix_view::theme::Style;
use helix_view::theme::{Modifier, Style};
use helix_view::view::ViewPosition;
use helix_view::Document;
use helix_view::Theme;
Expand Down Expand Up @@ -310,6 +310,8 @@ pub struct TextRenderer<'a> {
pub whitespace_style: Style,
pub indent_guide_char: String,
pub indent_guide_style: Style,
pub indent_guide_rainbow: RainbowIndentOptions,
pub theme: &'a Theme,
pub newline: String,
pub nbsp: String,
pub space: String,
Expand All @@ -326,7 +328,7 @@ impl<'a> TextRenderer<'a> {
pub fn new(
surface: &'a mut Surface,
doc: &Document,
theme: &Theme,
theme: &'a Theme,
col_offset: usize,
viewport: Rect,
) -> TextRenderer<'a> {
Expand Down Expand Up @@ -363,12 +365,19 @@ impl<'a> TextRenderer<'a> {
};

let text_style = theme.get("ui.text");
let basic_style = text_style.patch(
theme
.try_get("ui.virtual.indent-guide")
.unwrap_or_else(|| theme.get("ui.virtual.whitespace")),
);

let indent_width = doc.indent_style.indent_width(tab_width) as u16;

TextRenderer {
surface,
indent_guide_char: editor_config.indent_guides.character.into(),
indent_guide_rainbow: editor_config.indent_guides.rainbow_option.clone(),
theme,
newline,
nbsp,
space,
Expand All @@ -379,11 +388,7 @@ impl<'a> TextRenderer<'a> {
starting_indent: col_offset / indent_width as usize
+ (col_offset % indent_width as usize != 0) as usize
+ editor_config.indent_guides.skip_levels as usize,
indent_guide_style: text_style.patch(
theme
.try_get("ui.virtual.indent-guide")
.unwrap_or_else(|| theme.get("ui.virtual.whitespace")),
),
indent_guide_style: basic_style,
text_style,
draw_indent_guides: editor_config.indent_guides.render,
viewport,
Expand Down Expand Up @@ -477,8 +482,25 @@ impl<'a> TextRenderer<'a> {
as u16;
let y = self.viewport.y + row;
debug_assert!(self.surface.in_bounds(x, y));
self.surface
.set_string(x, y, &self.indent_guide_char, self.indent_guide_style);
match self.indent_guide_rainbow {
RainbowIndentOptions::None => {
self.surface
.set_string(x, y, &self.indent_guide_char, self.indent_guide_style)
}
RainbowIndentOptions::Dim => {
let new_style = self
.indent_guide_style
.patch(self.theme.get_rainbow(i))
.add_modifier(Modifier::DIM);
self.surface
.set_string(x, y, &self.indent_guide_char, new_style);
}
RainbowIndentOptions::Normal => {
let new_style = self.indent_guide_style.patch(self.theme.get_rainbow(i));
self.surface
.set_string(x, y, &self.indent_guide_char, new_style);
}
};
}
}
}
10 changes: 10 additions & 0 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -698,12 +698,21 @@ impl Default for WhitespaceCharacters {
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RainbowIndentOptions {
None,
Dim,
Normal,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct IndentGuidesConfig {
pub render: bool,
pub character: char,
pub skip_levels: u8,
pub rainbow_option: RainbowIndentOptions,
}

impl Default for IndentGuidesConfig {
Expand All @@ -712,6 +721,7 @@ impl Default for IndentGuidesConfig {
skip_levels: 0,
render: false,
character: '│',
rainbow_option: RainbowIndentOptions::None,
}
}
}
Expand Down
117 changes: 113 additions & 4 deletions helix-view/src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,17 +216,19 @@ pub struct Theme {
// tree-sitter highlight styles are stored in a Vec to optimize lookups
scopes: Vec<String>,
highlights: Vec<Style>,
rainbow_length: usize,
}

impl From<Value> for Theme {
fn from(value: Value) -> Self {
if let Value::Table(table) = value {
let (styles, scopes, highlights) = build_theme_values(table);
let (styles, scopes, highlights, rainbow_length) = build_theme_values(table);

Self {
styles,
scopes,
highlights,
rainbow_length,
..Default::default()
}
} else {
Expand All @@ -243,23 +245,25 @@ impl<'de> Deserialize<'de> for Theme {
{
let values = Map::<String, Value>::deserialize(deserializer)?;

let (styles, scopes, highlights) = build_theme_values(values);
let (styles, scopes, highlights, rainbow_length) = build_theme_values(values);

Ok(Self {
styles,
scopes,
highlights,
rainbow_length,
..Default::default()
})
}
}

fn build_theme_values(
mut values: Map<String, Value>,
) -> (HashMap<String, Style>, Vec<String>, Vec<Style>) {
) -> (HashMap<String, Style>, Vec<String>, Vec<Style>, usize) {
let mut styles = HashMap::new();
let mut scopes = Vec::new();
let mut highlights = Vec::new();
let mut rainbow_length = 0;

// TODO: alert user of parsing failures in editor
let palette = values
Expand All @@ -276,6 +280,27 @@ fn build_theme_values(
styles.reserve(values.len());
scopes.reserve(values.len());
highlights.reserve(values.len());

for (i, style) in values
.remove("rainbow")
.and_then(|value| match palette.parse_style_array(value) {
Ok(styles) => Some(styles),
Err(err) => {
warn!("{}", err);
None
}
})
.unwrap_or_else(default_rainbow)
.iter()
.enumerate()
{
let name = format!("rainbow.{}", i);
styles.insert(name.clone(), *style);
scopes.push(name);
highlights.push(*style);
rainbow_length += 1;
}

for (name, style_value) in values {
let mut style = Style::default();
if let Err(err) = palette.parse_style(&mut style, style_value) {
Expand All @@ -288,7 +313,7 @@ fn build_theme_values(
highlights.push(style);
}

(styles, scopes, highlights)
(styles, scopes, highlights, rainbow_length)
}

impl Theme {
Expand Down Expand Up @@ -349,6 +374,25 @@ impl Theme {
.all(|color| !matches!(color, Some(Color::Rgb(..))))
})
}

pub fn rainbow_length(&self) -> usize {
self.rainbow_length
}

pub fn get_rainbow(&self, index: usize) -> Style {
self.highlights[index % self.rainbow_length]
}
}

fn default_rainbow() -> Vec<Style> {
vec![
Style::default().fg(Color::Red),
Style::default().fg(Color::Yellow),
Style::default().fg(Color::Green),
Style::default().fg(Color::Blue),
Style::default().fg(Color::Cyan),
Style::default().fg(Color::Magenta),
]
}

struct ThemePalette {
Expand Down Expand Up @@ -479,6 +523,24 @@ impl ThemePalette {
}
Ok(())
}

/// Parses a TOML array into a [`Vec`] of [`Style`]. If the value cannot be
/// parsed as an array or if any style in the array cannot be parsed then an
/// error is returned.
pub fn parse_style_array(&self, value: Value) -> Result<Vec<Style>, String> {
let mut styles = Vec::new();

for v in value
.as_array()
.ok_or_else(|| format!("Theme: could not parse value as an array: '{}'", value))?
{
let mut style = Style::default();
self.parse_style(&mut style, v.clone())?;
styles.push(style);
}

Ok(styles)
}
}

impl TryFrom<Value> for ThemePalette {
Expand Down Expand Up @@ -553,4 +615,51 @@ mod tests {
.add_modifier(Modifier::BOLD)
);
}

#[test]
fn test_parse_valid_style_array() {
let theme = toml::toml! {
rainbow = ["#ff0000", "#ffa500", "#fff000", { fg = "#00ff00", modifiers = ["bold"] }]
};

let palette = ThemePalette::default();

let rainbow = theme.get("rainbow").unwrap();
let parse_result = palette.parse_style_array(rainbow.clone());

assert_eq!(
Ok(vec![
Style::default().fg(Color::Rgb(255, 0, 0)),
Style::default().fg(Color::Rgb(255, 165, 0)),
Style::default().fg(Color::Rgb(255, 240, 0)),
Style::default()
.fg(Color::Rgb(0, 255, 0))
.add_modifier(Modifier::BOLD),
]),
parse_result
)
}

#[test]
fn test_parse_invalid_style_array() {
let palette = ThemePalette::default();

let theme = toml::toml! { invalid_hex_code = ["#f00"] };
let invalid_hex_code = theme.get("invalid_hex_code").unwrap();
let parse_result = palette.parse_style_array(invalid_hex_code.clone());

assert_eq!(
Err("Theme: malformed hexcode: #f00".to_string()),
parse_result
);

let theme = toml::toml! { not_an_array = { red = "#ff0000" } };
let not_an_array = theme.get("not_an_array").unwrap();
let parse_result = palette.parse_style_array(not_an_array.clone());

assert_eq!(
Err("Theme: could not parse value as an array: '{ red = \"#ff0000\" }'".to_string()),
parse_result
)
}
}