Skip to main content

tree_house/
highlighter.rs

1use std::borrow::Cow;
2use std::cmp;
3use std::fmt;
4use std::mem::replace;
5use std::num::NonZeroU32;
6use std::ops::RangeBounds;
7use std::slice;
8use std::sync::Arc;
9
10use crate::config::{LanguageConfig, LanguageLoader};
11use crate::locals::ScopeCursor;
12use crate::query_iter::{MatchedNode, QueryIter, QueryIterEvent, QueryLoader};
13use crate::{Injection, Language, Layer, Syntax};
14use arc_swap::ArcSwap;
15use hashbrown::{HashMap, HashSet};
16use ropey::RopeSlice;
17use tree_sitter::{
18    query::{self, InvalidPredicateError, Query, UserPredicate},
19    Capture, Grammar,
20};
21use tree_sitter::{Pattern, QueryMatch};
22
23/// Contains the data needed to highlight code written in a particular language.
24///
25/// This struct is immutable and can be shared between threads.
26#[derive(Debug)]
27pub struct HighlightQuery {
28    pub query: Query,
29    highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
30    #[allow(dead_code)]
31    /// Patterns that do not match when the node is a local.
32    non_local_patterns: HashSet<Pattern>,
33    local_reference_capture: Option<Capture>,
34    /// The first pattern index that belongs to the locals query (i.e. the number of patterns
35    /// from the highlights query). Any pattern with index >= this value is from `locals.scm`.
36    first_locals_pattern: Pattern,
37}
38
39impl HighlightQuery {
40    pub(crate) fn new(
41        grammar: Grammar,
42        highlight_query_text: &str,
43        local_query_text: &str,
44    ) -> Result<Self, query::ParseError> {
45        // Concatenate the highlights and locals queries.
46        let mut query_source =
47            String::with_capacity(highlight_query_text.len() + local_query_text.len());
48        query_source.push_str(highlight_query_text);
49        query_source.push_str(local_query_text);
50
51        let mut non_local_patterns = HashSet::new();
52        let mut query = Query::new(grammar, &query_source, |pattern, predicate| {
53            match predicate {
54                // Allow the `(#set! local.scope-inherits <bool>)` property to be parsed.
55                // This information is not used by this query though, it's used in the
56                // injection query instead.
57                UserPredicate::SetProperty {
58                    key: "local.scope-inherits",
59                    ..
60                } => (),
61                // TODO: `(#is(-not)? local)` applies to the entire pattern. Ideally you
62                // should be able to supply capture(s?) which are each checked.
63                UserPredicate::IsPropertySet {
64                    negate: true,
65                    key: "local",
66                    val: None,
67                } => {
68                    non_local_patterns.insert(pattern);
69                }
70                _ => return Err(InvalidPredicateError::unknown(predicate)),
71            }
72            Ok(())
73        })?;
74
75        // Patterns with a start byte inside the highlights query source belong to
76        // highlights.scm; everything at or after belongs to locals.scm. Must be computed
77        // before disabling captures, which can invalidate `start_byte_for_pattern`.
78        let first_locals_pattern = query
79            .patterns()
80            .find(|&p| query.start_byte_for_pattern(p) >= highlight_query_text.len())
81            .unwrap_or(Pattern::SENTINEL);
82
83        // The highlight query only cares about local.reference captures. All scope and definition
84        // captures can be disabled.
85        query.disable_capture("local.scope");
86        let local_definition_captures: Vec<_> = query
87            .captures()
88            .filter(|&(_, name)| name.starts_with("local.definition."))
89            .map(|(_, name)| Box::<str>::from(name))
90            .collect();
91        for name in local_definition_captures {
92            query.disable_capture(&name);
93        }
94
95        Ok(Self {
96            highlight_indices: ArcSwap::from_pointee(vec![None; query.num_captures() as usize]),
97            non_local_patterns,
98            local_reference_capture: query.get_capture("local.reference"),
99            first_locals_pattern,
100            query,
101        })
102    }
103
104    /// Configures the list of recognized highlight names.
105    ///
106    /// Tree-sitter syntax-highlighting queries specify highlights in the form of dot-separated
107    /// highlight names like `punctuation.bracket` and `function.method.builtin`. Consumers of
108    /// these queries can choose to recognize highlights with different levels of specificity.
109    /// For example, the string `function.builtin` will match against `function.builtin.constructor`
110    /// but will not match `function.method.builtin` and `function.method`.
111    ///
112    /// The closure provided to this function should therefore try to first lookup the full
113    /// name. If no highlight was found for that name it should [`rsplit_once('.')`](str::rsplit_once)
114    /// and retry until a highlight has been found. If none of the parent scopes are defined
115    /// then `Highlight::NONE` should be returned.
116    ///
117    /// When highlighting, results are returned as `Highlight` values, configured by this function.
118    /// The meaning of these indices is up to the user of the implementation. The highlighter
119    /// treats the indices as entirely opaque.
120    pub(crate) fn configure(&self, f: &mut impl FnMut(&str) -> Option<Highlight>) {
121        let highlight_indices = self
122            .query
123            .captures()
124            .map(|(_, capture_name)| f(capture_name))
125            .collect();
126        self.highlight_indices.store(Arc::new(highlight_indices));
127    }
128}
129
130/// Indicates which highlight should be applied to a region of source code.
131///
132/// This type is represented as a non-max u32 - a u32 which cannot be `u32::MAX`. This is checked
133/// at runtime with assertions in `Highlight::new`.
134#[derive(Copy, Clone, PartialEq, Eq, Hash)]
135pub struct Highlight(NonZeroU32);
136
137impl Highlight {
138    pub const MAX: u32 = u32::MAX - 1;
139
140    pub const fn new(inner: u32) -> Self {
141        assert!(inner != u32::MAX);
142        // SAFETY: must be non-zero because `inner` is not `u32::MAX`.
143        Self(unsafe { NonZeroU32::new_unchecked(inner ^ u32::MAX) })
144    }
145
146    pub const fn get(&self) -> u32 {
147        self.0.get() ^ u32::MAX
148    }
149
150    pub const fn idx(&self) -> usize {
151        self.get() as usize
152    }
153}
154
155impl fmt::Debug for Highlight {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        f.debug_tuple("Highlight").field(&self.get()).finish()
158    }
159}
160
161#[derive(Debug)]
162struct HighlightedNode {
163    end: u32,
164    highlight: Highlight,
165}
166
167#[derive(Debug, Default)]
168pub struct LayerData {
169    parent_highlights: usize,
170    dormant_highlights: Vec<HighlightedNode>,
171}
172
173pub struct Highlighter<'a, 'tree, Loader: LanguageLoader> {
174    query: QueryIter<'a, 'tree, HighlightQueryLoader<&'a Loader>, ()>,
175    next_query_event: Option<QueryIterEvent<'tree, ()>>,
176    /// The stack of currently active highlights.
177    /// The ranges of the highlights stack, so each highlight in the Vec must have a starting
178    /// point `>=` the starting point of the next highlight in the Vec and and ending point `<=`
179    /// the ending point of the next highlight in the Vec.
180    ///
181    /// For a visual:
182    ///
183    /// ```text
184    ///     | C |
185    ///   |   B   |
186    /// |     A    |
187    /// ```
188    ///
189    /// would be `vec![A, B, C]`.
190    active_highlights: Vec<HighlightedNode>,
191    next_highlight_end: u32,
192    next_highlight_start: u32,
193    active_config: Option<&'a LanguageConfig>,
194    // The current layer and per-layer state could be tracked on the QueryIter itself (see
195    // `QueryIter::current_layer` and `QueryIter::layer_state`) however the highlighter peeks the
196    // query iter. The query iter is always one event ahead, so it will enter/exit injections
197    // before we get a chance to in the highlighter. So instead we track these on the highlighter.
198    // Also see `Self::advance_query_iter`.
199    current_layer: Layer,
200    layer_states: HashMap<Layer, LayerData>,
201}
202
203pub struct HighlightList<'a>(slice::Iter<'a, HighlightedNode>);
204
205impl Iterator for HighlightList<'_> {
206    type Item = Highlight;
207
208    fn next(&mut self) -> Option<Highlight> {
209        self.0.next().map(|node| node.highlight)
210    }
211
212    fn size_hint(&self) -> (usize, Option<usize>) {
213        self.0.size_hint()
214    }
215}
216
217impl DoubleEndedIterator for HighlightList<'_> {
218    fn next_back(&mut self) -> Option<Self::Item> {
219        self.0.next_back().map(|node| node.highlight)
220    }
221}
222
223impl ExactSizeIterator for HighlightList<'_> {
224    fn len(&self) -> usize {
225        self.0.len()
226    }
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub enum HighlightEvent {
231    /// Reset the active set of highlights to the given ones.
232    Refresh,
233    /// Add more highlights which build on the existing highlights.
234    Push,
235}
236
237impl<'a, 'tree: 'a, Loader: LanguageLoader> Highlighter<'a, 'tree, Loader> {
238    pub fn new(
239        syntax: &'tree Syntax,
240        src: RopeSlice<'a>,
241        loader: &'a Loader,
242        range: impl RangeBounds<u32>,
243    ) -> Self {
244        let mut query = QueryIter::new(syntax, src, HighlightQueryLoader(loader), range);
245        let active_language = query.current_language();
246        let mut res = Highlighter {
247            active_config: query.loader().0.get_config(active_language),
248            next_query_event: None,
249            current_layer: query.current_layer(),
250            layer_states: Default::default(),
251            active_highlights: Vec::new(),
252            next_highlight_end: u32::MAX,
253            next_highlight_start: 0,
254            query,
255        };
256        res.advance_query_iter();
257        res
258    }
259
260    pub fn active_highlights(&self) -> HighlightList<'_> {
261        HighlightList(self.active_highlights.iter())
262    }
263
264    pub fn next_event_offset(&self) -> u32 {
265        self.next_highlight_start.min(self.next_highlight_end)
266    }
267
268    pub fn advance(&mut self) -> (HighlightEvent, HighlightList<'_>) {
269        let mut refresh = false;
270
271        let pos = self.next_event_offset();
272        if self.next_highlight_end == pos {
273            self.process_highlight_end(pos);
274            refresh = true;
275        }
276
277        // Capture the boundary after any ended highlights are removed. Highlights
278        // at indices below this are from prior advance() calls and must not be
279        // replaced or removed by the current one. When refresh=true we return
280        // active_highlights.iter() anyway, so this value only matters for the
281        // search window in start_highlight and the Push slice when refresh=false.
282        let prev_stack_size = self.active_highlights.len();
283
284        let mut first_highlight = true;
285        let mut local_ref_candidate: Option<(u32, Highlight)> = None;
286        while self.next_highlight_start == pos {
287            let Some(query_event) = self.advance_query_iter() else {
288                break;
289            };
290            match query_event {
291                QueryIterEvent::EnterInjection(injection) => self.enter_injection(injection.layer),
292                QueryIterEvent::Match(node) => self.start_highlight(
293                    node,
294                    &mut first_highlight,
295                    prev_stack_size,
296                    &mut local_ref_candidate,
297                ),
298                QueryIterEvent::ExitInjection { injection, state } => {
299                    // `state` is returned if the layer is finished according to the `QueryIter`.
300                    // The highlighter should only consider a layer finished, though, when it also
301                    // has no remaining ranges to highlight. If the injection is combined and has
302                    // highlight(s) past this injection's range then we should deactivate it
303                    // (saving the highlights for the layer's next injection range) rather than
304                    // removing it.
305                    let layer_is_finished = state.is_some()
306                        && self
307                            .current_layer_highlights()
308                            .iter()
309                            .all(|h| h.end <= injection.range.end);
310                    if layer_is_finished {
311                        self.layer_states.remove(&injection.layer);
312                    } else {
313                        self.deactivate_layer(injection);
314                        refresh = true;
315                    }
316                    let active_language = self.query.syntax().layer(self.current_layer).language;
317                    self.active_config = self.query.loader().0.get_config(active_language);
318                }
319            }
320        }
321        // Commit any local reference candidate not cancelled by a discard pattern.
322        if let Some((end, highlight)) = local_ref_candidate {
323            let node = HighlightedNode { end, highlight };
324            let search_start = prev_stack_size.min(self.active_highlights.len());
325            let insert_position = self.active_highlights[search_start..]
326                .iter()
327                .rposition(|h| h.end <= end)
328                .map(|rel_idx| rel_idx + search_start);
329            match insert_position {
330                Some(idx) => match self.active_highlights[idx].end.cmp(&end) {
331                    cmp::Ordering::Equal => self.active_highlights[idx] = node,
332                    cmp::Ordering::Less => self.active_highlights.insert(idx, node),
333                    cmp::Ordering::Greater => unreachable!(),
334                },
335                None => self.active_highlights.extend(Some(node)),
336            }
337        }
338
339        self.next_highlight_end = self
340            .active_highlights
341            .last()
342            .map_or(u32::MAX, |node| node.end);
343
344        if refresh {
345            (
346                HighlightEvent::Refresh,
347                HighlightList(self.active_highlights.iter()),
348            )
349        } else {
350            (
351                HighlightEvent::Push,
352                HighlightList(self.active_highlights[prev_stack_size..].iter()),
353            )
354        }
355    }
356
357    fn advance_query_iter(&mut self) -> Option<QueryIterEvent<'tree, ()>> {
358        // Track the current layer **before** calling `QueryIter::next`. The QueryIter moves
359        // to the next event with `QueryIter::next` but we're treating that event as peeked - it
360        // hasn't occurred yet - so the current layer is the one the query iter was on _before_
361        // `QueryIter::next`.
362        self.current_layer = self.query.current_layer();
363        let event = replace(&mut self.next_query_event, self.query.next());
364        self.next_highlight_start = self
365            .next_query_event
366            .as_ref()
367            .map_or(u32::MAX, |event| event.start_byte());
368        event
369    }
370
371    fn process_highlight_end(&mut self, pos: u32) {
372        let i = self
373            .active_highlights
374            .iter()
375            .rposition(|highlight| highlight.end != pos)
376            .map_or(0, |i| i + 1);
377        self.active_highlights.truncate(i);
378    }
379
380    fn current_layer_highlights(&self) -> &[HighlightedNode] {
381        let parent_start = self
382            .layer_states
383            .get(&self.current_layer)
384            .map(|layer| layer.parent_highlights)
385            .unwrap_or_default()
386            .min(self.active_highlights.len());
387        &self.active_highlights[parent_start..]
388    }
389
390    fn enter_injection(&mut self, layer: Layer) {
391        debug_assert_eq!(layer, self.current_layer);
392        let active_language = self.query.syntax().layer(layer).language;
393        self.active_config = self.query.loader().0.get_config(active_language);
394
395        let state = self.layer_states.entry(layer).or_default();
396        state.parent_highlights = self.active_highlights.len();
397        self.active_highlights.append(&mut state.dormant_highlights);
398    }
399
400    fn deactivate_layer(&mut self, injection: Injection) {
401        let LayerData {
402            mut parent_highlights,
403            ref mut dormant_highlights,
404            ..
405        } = self.layer_states.get_mut(&injection.layer).unwrap();
406        parent_highlights = parent_highlights.min(self.active_highlights.len());
407        dormant_highlights.extend(self.active_highlights.drain(parent_highlights..));
408        self.process_highlight_end(injection.range.end);
409    }
410
411    fn start_highlight(
412        &mut self,
413        node: MatchedNode,
414        first_highlight: &mut bool,
415        prev_stack_size: usize,
416        local_ref_candidate: &mut Option<(u32, Highlight)>,
417    ) {
418        let range = node.node.byte_range();
419        // `<QueryIter as Iterator>::next` skips matches with empty ranges.
420        debug_assert!(
421            !range.is_empty(),
422            "QueryIter should not emit matches with empty ranges"
423        );
424
425        let config = self
426            .active_config
427            .expect("must have an active config to emit matches");
428
429        let is_local_reference =
430            Some(node.capture) == config.highlight_query.local_reference_capture;
431
432        // Captures from the locals.scm tier (pattern >= first_locals_pattern) that are not
433        // @local.reference act as discards: they cancel any pending local reference candidate
434        // for this node and leave the highlights.scm highlight intact.
435        if node.pattern >= config.highlight_query.first_locals_pattern && !is_local_reference {
436            if local_ref_candidate.is_some_and(|(end, _)| end == range.end) {
437                *local_ref_candidate = None;
438            }
439            return;
440        }
441
442        let highlight = if is_local_reference {
443            // If this capture was a `@local.reference` from the locals queries, look up the
444            // text of the node in the current locals cursor and use that highlight.
445            let text: Cow<str> = self
446                .query
447                .source()
448                .byte_slice(range.start as usize..range.end as usize)
449                .into();
450            let Some(definition) = self
451                .query
452                .syntax()
453                .layer(self.current_layer)
454                .locals
455                .lookup_reference(node.scope, &text)
456                .filter(|def| range.start >= def.range.end)
457            else {
458                return;
459            };
460            let highlight = config
461                .injection_query
462                .local_definition_captures
463                .load()
464                .get(&definition.capture)
465                .copied();
466            // Store as a candidate rather than replacing immediately. A later discard
467            // pattern in locals.scm can cancel this before it takes effect.
468            if let Some(h) = highlight {
469                *local_ref_candidate = Some((range.end, h));
470            }
471            return;
472        } else {
473            config.highlight_query.highlight_indices.load()[node.capture.idx()]
474        };
475
476        let highlight = highlight.map(|highlight| HighlightedNode {
477            end: range.end,
478            highlight,
479        });
480
481        // If multiple patterns match this exact node, prefer the last one which matched.
482        // This matches the precedence of Neovim, Zed, and tree-sitter-cli.
483        if !*first_highlight {
484            // NOTE: `!*first_highlight` implies that the start positions are the same.
485            // Only search within highlights added during this advance() call. Highlights
486            // before `prev_stack_size` belong to ancestor nodes from prior advance() calls
487            // and must not be replaced or removed by a descendant's captures.
488            let search_start = prev_stack_size.min(self.active_highlights.len());
489            let insert_position = self.active_highlights[search_start..]
490                .iter()
491                .rposition(|h| h.end <= range.end)
492                .map(|rel_idx| rel_idx + search_start);
493            if let Some(idx) = insert_position {
494                match self.active_highlights[idx].end.cmp(&range.end) {
495                    // If there is a prior highlight for this start..end range, replace it.
496                    cmp::Ordering::Equal => {
497                        if let Some(highlight) = highlight {
498                            self.active_highlights[idx] = highlight;
499                        } else {
500                            self.active_highlights.remove(idx);
501                        }
502                    }
503                    // Captures are emitted in the order that they are finished. Insert any
504                    // highlights which start at the same position into the active highlights so
505                    // that the ordering invariant remains satisfied.
506                    cmp::Ordering::Less => {
507                        if let Some(highlight) = highlight {
508                            self.active_highlights.insert(idx, highlight)
509                        }
510                    }
511                    // By definition of our `rposition` predicate:
512                    cmp::Ordering::Greater => unreachable!(),
513                }
514            } else {
515                self.active_highlights.extend(highlight);
516            }
517        } else if let Some(highlight) = highlight {
518            self.active_highlights.push(highlight);
519            *first_highlight = false;
520        }
521
522        // `active_highlights` must be a stack of highlight events the highlights stack on the
523        // prior highlights in the Vec. Each highlight's range must be a subset of the highlight's
524        // range before it.
525        debug_assert!(
526            // The assertion is actually true for the entire stack but combined injections
527            // throw a wrench in things: the highlight can end after the current injection.
528            // The highlight is removed from `active_highlights` as the injection layer ends
529            // so the wider assertion would be true in practice. We don't track the injection
530            // end right here though so we can't assert on it.
531            self.current_layer_highlights().is_sorted_by_key(|h| cmp::Reverse(h.end)),
532            "unsorted highlights on layer {:?}: {:?}\nall active highlights must be sorted by `end` descending",
533            self.current_layer,
534            self.active_highlights,
535        );
536    }
537}
538
539pub(crate) struct HighlightQueryLoader<T>(T);
540
541impl<'a, T: LanguageLoader> QueryLoader<'a> for HighlightQueryLoader<&'a T> {
542    fn get_query(&mut self, lang: Language) -> Option<&'a Query> {
543        self.0
544            .get_config(lang)
545            .map(|config| &config.highlight_query.query)
546    }
547
548    fn are_predicates_satisfied(
549        &self,
550        lang: Language,
551        mat: &QueryMatch<'_, '_>,
552        source: RopeSlice<'_>,
553        locals_cursor: &ScopeCursor<'_>,
554    ) -> bool {
555        let highlight_query = &self
556            .0
557            .get_config(lang)
558            .expect("must have a config to emit matches")
559            .highlight_query;
560
561        // Highlight queries should reject the match when a pattern is marked with
562        // `(#is-not? local)` and any capture in the pattern matches a definition in scope.
563        //
564        // TODO: in the future we should propose that `#is-not? local` takes one or more
565        // captures as arguments. Ideally we would check that the captured node is also captured
566        // by a `local.reference` capture from the locals query but that's really messy to pass
567        // around that information. For now we assume that all matches in the pattern are also
568        // captured as `local.reference` in the locals, which covers most cases.
569        if highlight_query.local_reference_capture.is_some()
570            && highlight_query.non_local_patterns.contains(&mat.pattern())
571        {
572            let has_local_reference = mat.matched_nodes().any(|n| {
573                let range = n.node.byte_range();
574                let text: Cow<str> = source
575                    .byte_slice(range.start as usize..range.end as usize)
576                    .into();
577                locals_cursor
578                    .locals
579                    .lookup_reference(locals_cursor.current_scope(), &text)
580                    .is_some_and(|def| range.start >= def.range.start)
581            });
582            if has_local_reference {
583                return false;
584            }
585        }
586
587        true
588    }
589}