Skip to content

Commit d5a32ba

Browse files
committed
🌈
1 parent 718c3ba commit d5a32ba

File tree

3 files changed

+130
-5
lines changed

3 files changed

+130
-5
lines changed

‎helix-core/src/syntax.rs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,8 @@ impl Syntax {
950950
layers,
951951
next_event: None,
952952
last_highlight_range: None,
953+
match_highlights: HashMap::new(),
954+
rainbow_nesting_level: 0,
953955
};
954956
result.sort_layers();
955957
result
@@ -1163,6 +1165,9 @@ pub struct HighlightConfiguration {
11631165
local_def_capture_index: Option<u32>,
11641166
local_def_value_capture_index: Option<u32>,
11651167
local_ref_capture_index: Option<u32>,
1168+
rainbow_capture_index: Option<u32>,
1169+
/// The number of rainbow styles in the current theme.
1170+
rainbow_styles: AtomicUsize,
11661171
}
11671172

11681173
#[derive(Debug)]
@@ -1188,6 +1193,11 @@ struct HighlightIter<'a> {
11881193
iter_count: usize,
11891194
next_event: Option<HighlightEvent>,
11901195
last_highlight_range: Option<(usize, usize, usize)>,
1196+
/// A mapping between tree-sitter `QueryMatch` IDs and `Highlights`.
1197+
/// This can be used to re-use a single highlight for an entire match
1198+
/// like with rainbow matches.
1199+
match_highlights: HashMap<u32, Highlight>,
1200+
rainbow_nesting_level: usize,
11911201
}
11921202

11931203
// Adapter to convert rope chunks to bytes
@@ -1307,13 +1317,15 @@ impl HighlightConfiguration {
13071317
let mut local_def_value_capture_index = None;
13081318
let mut local_ref_capture_index = None;
13091319
let mut local_scope_capture_index = None;
1320+
let mut rainbow_capture_index = None;
13101321
for (i, name) in query.capture_names().iter().enumerate() {
13111322
let i = Some(i as u32);
13121323
match name.as_str() {
13131324
"local.definition" => local_def_capture_index = i,
13141325
"local.definition-value" => local_def_value_capture_index = i,
13151326
"local.reference" => local_ref_capture_index = i,
13161327
"local.scope" => local_scope_capture_index = i,
1328+
"rainbow" => rainbow_capture_index = i,
13171329
_ => {}
13181330
}
13191331
}
@@ -1342,6 +1354,8 @@ impl HighlightConfiguration {
13421354
local_def_capture_index,
13431355
local_def_value_capture_index,
13441356
local_ref_capture_index,
1357+
rainbow_capture_index,
1358+
rainbow_styles: AtomicUsize::new(0),
13451359
})
13461360
}
13471361

@@ -1373,7 +1387,6 @@ impl HighlightConfiguration {
13731387
let mut best_index = None;
13741388
let mut best_match_len = 0;
13751389
for (i, recognized_name) in recognized_names.iter().enumerate() {
1376-
let recognized_name = recognized_name;
13771390
let mut len = 0;
13781391
let mut matches = true;
13791392
for part in recognized_name.split('.') {
@@ -1393,6 +1406,13 @@ impl HighlightConfiguration {
13931406
.collect();
13941407

13951408
self.highlight_indices.store(Arc::new(indices));
1409+
1410+
let rainbow_styles = recognized_names
1411+
.iter()
1412+
.filter(|name| name.starts_with("rainbow."))
1413+
.count();
1414+
1415+
self.rainbow_styles.store(rainbow_styles, Ordering::Relaxed);
13961416
}
13971417
}
13981418

@@ -1765,7 +1785,46 @@ impl<'a> Iterator for HighlightIter<'a> {
17651785
}
17661786
}
17671787

1768-
let current_highlight = layer.config.highlight_indices.load()[capture.index as usize];
1788+
// If the capture corresponds to the `@rainbow` scope, lookup the match
1789+
// in the `match_highlights` map. Otherwise, use the highlight from
1790+
// attempt to replace its default
1791+
let rainbow_highlight = if layer.config.rainbow_capture_index == Some(capture.index) {
1792+
let rainbow_styles = layer.config.rainbow_styles.load(Ordering::Relaxed);
1793+
1794+
if rainbow_styles > 0 {
1795+
if capture_index == 0 {
1796+
// Initial capture in the match, add the entry and increment
1797+
// nesting level, wrapping around to the first rainbow color.
1798+
let next_highlight = Highlight(self.rainbow_nesting_level);
1799+
1800+
self.rainbow_nesting_level =
1801+
(self.rainbow_nesting_level + 1) % rainbow_styles;
1802+
1803+
self.match_highlights.insert(match_.id(), next_highlight);
1804+
1805+
Some(next_highlight)
1806+
} else if capture_index == match_.captures.len() - 1 {
1807+
// Final capture in the match, remove the entry and decrement
1808+
// nesting level, wrapping around to the last rainbow color.
1809+
self.rainbow_nesting_level = self
1810+
.rainbow_nesting_level
1811+
.checked_sub(1)
1812+
.unwrap_or(rainbow_styles - 1);
1813+
1814+
self.match_highlights.remove(&match_.id())
1815+
} else {
1816+
// Any nodes between the first and last re-use the highlight.
1817+
self.match_highlights.get(&match_.id()).copied()
1818+
}
1819+
} else {
1820+
None
1821+
}
1822+
} else {
1823+
None
1824+
};
1825+
1826+
let current_highlight = rainbow_highlight
1827+
.or_else(|| layer.config.highlight_indices.load()[capture.index as usize]);
17691828

17701829
// If this node represents a local definition, then store the current
17711830
// highlight value on the local scope entry representing this node.

‎helix-view/src/editor.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,16 @@ pub struct Config {
155155
#[serde(default)]
156156
pub search: SearchConfig,
157157
pub lsp: LspConfig,
158-
/// Column numbers at which to draw the rulers. Default to `[]`, meaning no rulers.
158+
/// Column numbers at which to draw the rulers. Defaults to `[]`, meaning no rulers.
159159
pub rulers: Vec<u16>,
160160
#[serde(default)]
161161
pub whitespace: WhitespaceConfig,
162162
/// Vertical indent width guides.
163163
pub indent_guides: IndentGuidesConfig,
164164
/// Whether to color modes with different colors. Defaults to `false`.
165165
pub color_modes: bool,
166+
/// Whether to highlight rainbow brackets. Defaults to `false`.
167+
pub rainbow_brackets: bool,
166168
}
167169

168170
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
@@ -417,6 +419,8 @@ impl Default for Config {
417419
whitespace: WhitespaceConfig::default(),
418420
indent_guides: IndentGuidesConfig::default(),
419421
color_modes: false,
422+
// FIXME set me to `false` before un-drafting the PR
423+
rainbow_brackets: true,
420424
}
421425
}
422426
}
@@ -646,8 +650,13 @@ impl Editor {
646650
return;
647651
}
648652

649-
let scopes = theme.scopes();
650-
self.syn_loader.set_scopes(scopes.to_vec());
653+
let theme = if self.config().rainbow_brackets {
654+
theme.with_rainbow()
655+
} else {
656+
theme
657+
};
658+
659+
self.syn_loader.set_scopes(theme.scopes().to_vec());
651660

652661
match preview {
653662
ThemeAction::Preview => {

‎helix-view/src/theme.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ pub struct Theme {
103103
// tree-sitter highlight styles are stored in a Vec to optimize lookups
104104
scopes: Vec<String>,
105105
highlights: Vec<Style>,
106+
rainbow: Vec<Style>,
106107
}
107108

108109
impl<'de> Deserialize<'de> for Theme {
@@ -113,6 +114,7 @@ impl<'de> Deserialize<'de> for Theme {
113114
let mut styles = HashMap::new();
114115
let mut scopes = Vec::new();
115116
let mut highlights = Vec::new();
117+
let mut rainbows = Vec::new();
116118

117119
if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
118120
// TODO: alert user of parsing failures in editor
@@ -126,6 +128,19 @@ impl<'de> Deserialize<'de> for Theme {
126128
})
127129
.unwrap_or_default();
128130

131+
for style_value in colors
132+
.remove("rainbow")
133+
.and_then(|v| v.as_array().cloned())
134+
.unwrap_or_default()
135+
.iter()
136+
{
137+
let mut style = Style::default();
138+
match palette.parse_style(&mut style, style_value.clone()) {
139+
Ok(()) => rainbows.push(style),
140+
Err(err) => warn!("{}", err),
141+
}
142+
}
143+
129144
styles.reserve(colors.len());
130145
scopes.reserve(colors.len());
131146
highlights.reserve(colors.len());
@@ -147,6 +162,7 @@ impl<'de> Deserialize<'de> for Theme {
147162
scopes,
148163
styles,
149164
highlights,
165+
rainbow: rainbows,
150166
})
151167
}
152168
}
@@ -185,6 +201,47 @@ impl Theme {
185201
.all(|color| !matches!(color, Some(Color::Rgb(..))))
186202
})
187203
}
204+
205+
/// Returns a new theme with rainbow scopes enabled.
206+
///
207+
/// Rainbow scopes are prepended to the `scopes` and `highlights` `Vec`s
208+
/// and merged into the `styles` `HashMap`.
209+
pub fn with_rainbow(&self) -> Self {
210+
let mut scopes = Vec::new();
211+
let mut styles = self.styles.clone();
212+
213+
let rainbow = if self.rainbow.is_empty() {
214+
// Default rainbow
215+
vec![
216+
Style::default().fg(Color::Red),
217+
Style::default().fg(Color::Yellow),
218+
Style::default().fg(Color::Green),
219+
Style::default().fg(Color::Blue),
220+
Style::default().fg(Color::Cyan),
221+
Style::default().fg(Color::Magenta),
222+
]
223+
} else {
224+
self.rainbow.clone()
225+
};
226+
227+
let mut highlights = rainbow.clone();
228+
229+
for (i, style) in rainbow.iter().enumerate() {
230+
let name = format!("rainbow.{}", i);
231+
styles.insert(name.clone(), *style);
232+
scopes.push(name);
233+
}
234+
235+
scopes.extend(self.scopes.clone());
236+
highlights.extend(self.highlights.clone());
237+
238+
Self {
239+
scopes,
240+
styles,
241+
highlights,
242+
rainbow,
243+
}
244+
}
188245
}
189246

190247
struct ThemePalette {

0 commit comments

Comments
 (0)