Skip to content

Commit 56c3852

Browse files
committed
Add rainbow highlights on top of tree-house bindings
1 parent 813f771 commit 56c3852

File tree

36 files changed

+844
-5
lines changed

36 files changed

+844
-5
lines changed

helix-core/src/syntax.rs

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@ use std::{
1313
use anyhow::{Context, Result};
1414
use arc_swap::{ArcSwap, Guard};
1515
use config::{Configuration, FileType, LanguageConfiguration, LanguageServerConfiguration};
16+
use foldhash::HashSet;
1617
use helix_loader::grammar::get_language;
1718
use helix_stdx::rope::RopeSliceExt as _;
1819
use once_cell::sync::OnceCell;
1920
use ropey::RopeSlice;
2021
use tree_house::{
2122
highlighter,
22-
query_iter::QueryIter,
23-
tree_sitter::{Grammar, InactiveQueryCursor, InputEdit, Node, Query, RopeInput, Tree},
23+
query_iter::{QueryIter, QueryIterEvent},
24+
tree_sitter::{
25+
query::{InvalidPredicateError, UserPredicate},
26+
Capture, Grammar, InactiveQueryCursor, InputEdit, Node, Pattern, Query, RopeInput, Tree,
27+
},
2428
Error, InjectionLanguageMarker, LanguageConfig as SyntaxConfig, Layer,
2529
};
2630

@@ -37,6 +41,7 @@ pub struct LanguageData {
3741
syntax: OnceCell<Option<SyntaxConfig>>,
3842
indent_query: OnceCell<Option<IndentQuery>>,
3943
textobject_query: OnceCell<Option<TextObjectQuery>>,
44+
rainbow_query: OnceCell<Option<RainbowQuery>>,
4045
}
4146

4247
impl LanguageData {
@@ -46,6 +51,7 @@ impl LanguageData {
4651
syntax: OnceCell::new(),
4752
indent_query: OnceCell::new(),
4853
textobject_query: OnceCell::new(),
54+
rainbow_query: OnceCell::new(),
4955
}
5056
}
5157

@@ -154,6 +160,36 @@ impl LanguageData {
154160
.as_ref()
155161
}
156162

163+
/// Compiles the rainbows.scm query for a language.
164+
/// This function should only be used by this module or the xtask crate.
165+
pub fn compile_rainbow_query(
166+
grammar: Grammar,
167+
config: &LanguageConfiguration,
168+
) -> Result<Option<RainbowQuery>> {
169+
let name = &config.language_id;
170+
let text = read_query(name, "rainbows.scm");
171+
if text.is_empty() {
172+
return Ok(None);
173+
}
174+
let rainbow_query = RainbowQuery::new(grammar, &text)
175+
.with_context(|| format!("Failed to compile rainbows.scm query for '{name}'"))?;
176+
Ok(Some(rainbow_query))
177+
}
178+
179+
fn rainbow_query(&self, loader: &Loader) -> Option<&RainbowQuery> {
180+
self.rainbow_query
181+
.get_or_init(|| {
182+
let grammar = self.syntax_config(loader)?.grammar;
183+
Self::compile_rainbow_query(grammar, &self.config)
184+
.map_err(|err| {
185+
log::error!("{err}");
186+
})
187+
.ok()
188+
.flatten()
189+
})
190+
.as_ref()
191+
}
192+
157193
fn reconfigure(&self, scopes: &[String]) {
158194
if let Some(Some(config)) = self.syntax.get() {
159195
reconfigure_highlights(config, scopes);
@@ -324,6 +360,10 @@ impl Loader {
324360
self.language(lang).textobject_query(self)
325361
}
326362

363+
fn rainbow_query(&self, lang: Language) -> Option<&RainbowQuery> {
364+
self.language(lang).rainbow_query(self)
365+
}
366+
327367
pub fn language_server_configs(&self) -> &HashMap<String, LanguageServerConfiguration> {
328368
&self.language_server_configs
329369
}
@@ -496,6 +536,79 @@ impl Syntax {
496536
{
497537
QueryIter::new(&self.inner, source, loader, range)
498538
}
539+
540+
pub fn rainbow_highlights(
541+
&self,
542+
source: RopeSlice,
543+
rainbow_length: usize,
544+
loader: &Loader,
545+
range: impl RangeBounds<u32>,
546+
) -> OverlayHighlights {
547+
struct RainbowScope<'tree> {
548+
end: u32,
549+
node: Option<Node<'tree>>,
550+
highlight: Highlight,
551+
}
552+
553+
let mut scope_stack = Vec::<RainbowScope>::new();
554+
let mut highlights = Vec::new();
555+
let mut query_iter = self.query_iter::<_, (), _>(
556+
source,
557+
|lang| loader.rainbow_query(lang).map(|q| &q.query),
558+
range,
559+
);
560+
561+
while let Some(event) = query_iter.next() {
562+
let QueryIterEvent::Match(mat) = event else {
563+
continue;
564+
};
565+
566+
let rainbow_query = loader
567+
.rainbow_query(query_iter.current_language())
568+
.expect("language must have a rainbow query to emit matches");
569+
570+
let byte_range = mat.node.byte_range();
571+
// Pop any scopes that end before this capture begins.
572+
while scope_stack
573+
.last()
574+
.is_some_and(|scope| byte_range.start >= scope.end)
575+
{
576+
scope_stack.pop();
577+
}
578+
579+
let capture = Some(mat.capture);
580+
if capture == rainbow_query.scope_capture {
581+
scope_stack.push(RainbowScope {
582+
end: byte_range.end,
583+
node: if rainbow_query
584+
.include_children_patterns
585+
.contains(&mat.pattern)
586+
{
587+
None
588+
} else {
589+
Some(mat.node.clone())
590+
},
591+
highlight: Highlight::new((scope_stack.len() % rainbow_length) as u32),
592+
});
593+
} else if capture == rainbow_query.bracket_capture {
594+
if let Some(scope) = scope_stack.last() {
595+
if !scope
596+
.node
597+
.as_ref()
598+
.is_some_and(|node| mat.node.parent().as_ref() != Some(node))
599+
{
600+
let start = source
601+
.byte_to_char(source.floor_char_boundary(byte_range.start as usize));
602+
let end =
603+
source.byte_to_char(source.ceil_char_boundary(byte_range.end as usize));
604+
highlights.push((scope.highlight, start..end));
605+
}
606+
}
607+
}
608+
}
609+
610+
OverlayHighlights::Heterogenous { highlights }
611+
}
499612
}
500613

501614
pub type Highlighter<'a> = highlighter::Highlighter<'a, 'a, Loader>;
@@ -939,6 +1052,57 @@ fn pretty_print_tree_impl<W: fmt::Write>(
9391052
Ok(())
9401053
}
9411054

1055+
/// Finds the child of `node` which contains the given byte range.
1056+
1057+
pub fn child_for_byte_range<'a>(node: &Node<'a>, range: ops::Range<u32>) -> Option<Node<'a>> {
1058+
for child in node.children() {
1059+
let child_range = child.byte_range();
1060+
1061+
if range.start >= child_range.start && range.end <= child_range.end {
1062+
return Some(child);
1063+
}
1064+
}
1065+
1066+
None
1067+
}
1068+
1069+
#[derive(Debug)]
1070+
pub struct RainbowQuery {
1071+
query: Query,
1072+
include_children_patterns: HashSet<Pattern>,
1073+
scope_capture: Option<Capture>,
1074+
bracket_capture: Option<Capture>,
1075+
}
1076+
1077+
impl RainbowQuery {
1078+
fn new(grammar: Grammar, source: &str) -> Result<Self, tree_sitter::query::ParseError> {
1079+
let mut include_children_patterns = HashSet::default();
1080+
1081+
let query = Query::new(grammar, source, |pattern, predicate| match predicate {
1082+
UserPredicate::SetProperty {
1083+
key: "rainbow.include-children",
1084+
val,
1085+
} => {
1086+
if val.is_some() {
1087+
return Err(
1088+
"property 'rainbow.include-children' does not take an argument".into(),
1089+
);
1090+
}
1091+
include_children_patterns.insert(pattern);
1092+
Ok(())
1093+
}
1094+
_ => Err(InvalidPredicateError::unknown(predicate)),
1095+
})?;
1096+
1097+
Ok(Self {
1098+
include_children_patterns,
1099+
scope_capture: query.get_capture("rainbow.scope"),
1100+
bracket_capture: query.get_capture("rainbow.bracket"),
1101+
query,
1102+
})
1103+
}
1104+
}
1105+
9421106
#[cfg(test)]
9431107
mod test {
9441108
use once_cell::sync::Lazy;

helix-core/src/syntax/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ pub struct LanguageConfiguration {
9898
pub workspace_lsp_roots: Option<Vec<PathBuf>>,
9999
#[serde(default)]
100100
pub persistent_diagnostic_sources: Vec<String>,
101+
/// Overrides the `editor.rainbow-brackets` config key for the language.
102+
pub rainbow_brackets: Option<bool>,
101103
}
102104

103105
impl LanguageConfiguration {

helix-term/src/ui/editor.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ impl EditorView {
127127
&text_annotations,
128128
));
129129

130+
if doc
131+
.language_config()
132+
.and_then(|config| config.rainbow_brackets)
133+
.unwrap_or(config.rainbow_brackets)
134+
{
135+
if let Some(overlay) =
136+
Self::doc_rainbow_highlights(doc, view_offset.anchor, inner.height, theme, &loader)
137+
{
138+
overlays.push(overlay);
139+
}
140+
}
141+
130142
Self::doc_diagnostics_highlights_into(doc, theme, &mut overlays);
131143

132144
if is_focused {
@@ -304,6 +316,27 @@ impl EditorView {
304316
text_annotations.collect_overlay_highlights(range)
305317
}
306318

319+
pub fn doc_rainbow_highlights(
320+
doc: &Document,
321+
anchor: usize,
322+
height: u16,
323+
theme: &Theme,
324+
loader: &syntax::Loader,
325+
) -> Option<OverlayHighlights> {
326+
let syntax = doc.syntax()?;
327+
let text = doc.text().slice(..);
328+
let row = text.char_to_line(anchor.min(text.len_chars()));
329+
let visible_range = Self::viewport_byte_range(text, row, height);
330+
let start = syntax::child_for_byte_range(
331+
&syntax.tree().root_node(),
332+
visible_range.start as u32..visible_range.end as u32,
333+
)
334+
.map_or(visible_range.start as u32, |node| node.start_byte());
335+
let range = start..visible_range.end as u32;
336+
337+
Some(syntax.rainbow_highlights(text, theme.rainbow_length(), loader, range))
338+
}
339+
307340
/// Get highlight spans for document diagnostics
308341
pub fn doc_diagnostics_highlights_into(
309342
doc: &Document,

helix-view/src/editor.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,8 @@ pub struct Config {
373373
/// Whether to read settings from [EditorConfig](https://editorconfig.org) files. Defaults to
374374
/// `true`.
375375
pub editor_config: bool,
376+
/// Whether to render rainbow colors for matching brackets. Defaults to `false`.
377+
pub rainbow_brackets: bool,
376378
}
377379

378380
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
@@ -1020,6 +1022,7 @@ impl Default for Config {
10201022
end_of_line_diagnostics: DiagnosticFilter::Disable,
10211023
clipboard_provider: ClipboardProvider::default(),
10221024
editor_config: true,
1025+
rainbow_brackets: false,
10231026
}
10241027
}
10251028
}

0 commit comments

Comments
 (0)