gpui_component/highlighter/
highlighter.rs

1use crate::highlighter::{HighlightTheme, LanguageRegistry};
2use crate::input::RopeExt;
3
4use anyhow::{anyhow, Context, Result};
5use gpui::{HighlightStyle, SharedString};
6
7use ropey::{ChunkCursor, Rope};
8use std::{
9    collections::{BTreeSet, HashMap},
10    ops::Range,
11    usize,
12};
13use sum_tree::Bias;
14use tree_sitter::{
15    InputEdit, Node, Parser, Point, Query, QueryCursor, QueryMatch, StreamingIterator, Tree,
16};
17
18/// A syntax highlighter that supports incremental parsing, multiline text,
19/// and caching of highlight results.
20#[allow(unused)]
21pub struct SyntaxHighlighter {
22    language: SharedString,
23    query: Option<Query>,
24    injection_queries: HashMap<SharedString, Query>,
25
26    locals_pattern_index: usize,
27    highlights_pattern_index: usize,
28    // highlight_indices: Vec<Option<Highlight>>,
29    non_local_variable_patterns: Vec<bool>,
30    injection_content_capture_index: Option<u32>,
31    injection_language_capture_index: Option<u32>,
32    local_scope_capture_index: Option<u32>,
33    local_def_capture_index: Option<u32>,
34    local_def_value_capture_index: Option<u32>,
35    local_ref_capture_index: Option<u32>,
36
37    /// The last parsed source text.
38    text: Rope,
39    parser: Parser,
40    /// The last parsed tree.
41    tree: Option<Tree>,
42}
43
44struct TextProvider<'a>(&'a Rope);
45struct ByteChunks<'a> {
46    cursor: ChunkCursor<'a>,
47    end: usize,
48}
49impl<'a> tree_sitter::TextProvider<&'a [u8]> for TextProvider<'a> {
50    type I = ByteChunks<'a>;
51
52    fn text(&mut self, node: tree_sitter::Node) -> Self::I {
53        let range = node.byte_range();
54        let cursor = self.0.chunk_cursor_at(range.start);
55
56        ByteChunks {
57            cursor,
58            end: range.end,
59        }
60    }
61}
62
63impl<'a> Iterator for ByteChunks<'a> {
64    type Item = &'a [u8];
65
66    fn next(&mut self) -> Option<Self::Item> {
67        let cursor = &mut self.cursor;
68        let end = self.end;
69
70        if cursor.next() && cursor.byte_offset() < end {
71            Some(cursor.chunk().as_bytes())
72        } else {
73            None
74        }
75    }
76}
77
78#[derive(Debug, Default, Clone)]
79struct HighlightSummary {
80    count: usize,
81    start: usize,
82    end: usize,
83    min_start: usize,
84    max_end: usize,
85}
86
87/// The highlight item, the range is offset of the token in the tree.
88#[derive(Debug, Default, Clone)]
89struct HighlightItem {
90    /// The byte range of the highlight in the text.
91    range: Range<usize>,
92    /// The highlight name, like `function`, `string`, `comment`, etc.
93    name: SharedString,
94}
95
96impl HighlightItem {
97    pub fn new(range: Range<usize>, name: impl Into<SharedString>) -> Self {
98        Self {
99            range,
100            name: name.into(),
101        }
102    }
103}
104
105impl sum_tree::Item for HighlightItem {
106    type Summary = HighlightSummary;
107    fn summary(&self, _cx: &()) -> Self::Summary {
108        HighlightSummary {
109            count: 1,
110            start: self.range.start,
111            end: self.range.end,
112            min_start: self.range.start,
113            max_end: self.range.end,
114        }
115    }
116}
117
118impl sum_tree::Summary for HighlightSummary {
119    type Context<'a> = &'a ();
120    fn zero(_: Self::Context<'_>) -> Self {
121        HighlightSummary {
122            count: 0,
123            start: usize::MIN,
124            end: usize::MAX,
125            min_start: usize::MAX,
126            max_end: usize::MIN,
127        }
128    }
129
130    fn add_summary(&mut self, other: &Self, _: Self::Context<'_>) {
131        self.min_start = self.min_start.min(other.min_start);
132        self.max_end = self.max_end.max(other.max_end);
133        self.start = other.start;
134        self.end = other.end;
135        self.count += other.count;
136    }
137}
138
139impl<'a> sum_tree::Dimension<'a, HighlightSummary> for usize {
140    fn zero(_: &()) -> Self {
141        0
142    }
143
144    fn add_summary(&mut self, _: &'a HighlightSummary, _: &()) {}
145}
146
147impl<'a> sum_tree::Dimension<'a, HighlightSummary> for Range<usize> {
148    fn zero(_: &()) -> Self {
149        Default::default()
150    }
151
152    fn add_summary(&mut self, summary: &'a HighlightSummary, _: &()) {
153        self.start = summary.start;
154        self.end = summary.end;
155    }
156}
157
158impl SyntaxHighlighter {
159    /// Create a new SyntaxHighlighter for HTML.
160    pub fn new(lang: &str) -> Self {
161        match Self::build_combined_injections_query(&lang) {
162            Ok(result) => result,
163            Err(err) => {
164                tracing::warn!(
165                    "SyntaxHighlighter init failed, fallback to use `text`, {}",
166                    err
167                );
168                Self::build_combined_injections_query("text").unwrap()
169            }
170        }
171    }
172
173    /// Build the combined injections query for the given language.
174    ///
175    /// https://github.com/tree-sitter/tree-sitter/blob/v0.25.5/highlight/src/lib.rs#L336
176    fn build_combined_injections_query(lang: &str) -> Result<Self> {
177        let Some(config) = LanguageRegistry::singleton().language(&lang) else {
178            return Err(anyhow!(
179                "language {:?} is not registered in `LanguageRegistry`",
180                lang
181            ));
182        };
183
184        let mut parser = Parser::new();
185        parser
186            .set_language(&config.language)
187            .context("parse set_language")?;
188
189        // Concatenate the query strings, keeping track of the start offset of each section.
190        let mut query_source = String::new();
191        query_source.push_str(&config.injections);
192        let locals_query_offset = query_source.len();
193        query_source.push_str(&config.locals);
194        let highlights_query_offset = query_source.len();
195        query_source.push_str(&config.highlights);
196
197        // Construct a single query by concatenating the three query strings, but record the
198        // range of pattern indices that belong to each individual string.
199        let query = Query::new(&config.language, &query_source).context("new query")?;
200
201        let mut locals_pattern_index = 0;
202        let mut highlights_pattern_index = 0;
203        for i in 0..(query.pattern_count()) {
204            let pattern_offset = query.start_byte_for_pattern(i);
205            if pattern_offset < highlights_query_offset {
206                if pattern_offset < highlights_query_offset {
207                    highlights_pattern_index += 1;
208                }
209                if pattern_offset < locals_query_offset {
210                    locals_pattern_index += 1;
211                }
212            }
213        }
214
215        // let Some(mut combined_injections_query) =
216        //     Query::new(&config.language, &config.injections).ok()
217        // else {
218        //     return None;
219        // };
220
221        // let mut has_combined_queries = false;
222        // for pattern_index in 0..locals_pattern_index {
223        //     let settings = query.property_settings(pattern_index);
224        //     if settings.iter().any(|s| &*s.key == "injection.combined") {
225        //         has_combined_queries = true;
226        //         query.disable_pattern(pattern_index);
227        //     } else {
228        //         combined_injections_query.disable_pattern(pattern_index);
229        //     }
230        // }
231        // let combined_injections_query = if has_combined_queries {
232        //     Some(combined_injections_query)
233        // } else {
234        //     None
235        // };
236
237        // Find all of the highlighting patterns that are disabled for nodes that
238        // have been identified as local variables.
239        let non_local_variable_patterns = (0..query.pattern_count())
240            .map(|i| {
241                query
242                    .property_predicates(i)
243                    .iter()
244                    .any(|(prop, positive)| !*positive && prop.key.as_ref() == "local")
245            })
246            .collect();
247
248        // Store the numeric ids for all of the special captures.
249        let mut injection_content_capture_index = None;
250        let mut injection_language_capture_index = None;
251        let mut local_def_capture_index = None;
252        let mut local_def_value_capture_index = None;
253        let mut local_ref_capture_index = None;
254        let mut local_scope_capture_index = None;
255        for (i, name) in query.capture_names().iter().enumerate() {
256            let i = Some(i as u32);
257            match *name {
258                "injection.content" => injection_content_capture_index = i,
259                "injection.language" => injection_language_capture_index = i,
260                "local.definition" => local_def_capture_index = i,
261                "local.definition-value" => local_def_value_capture_index = i,
262                "local.reference" => local_ref_capture_index = i,
263                "local.scope" => local_scope_capture_index = i,
264                _ => {}
265            }
266        }
267
268        let mut injection_queries = HashMap::new();
269        for inj_language in config.injection_languages.iter() {
270            if let Some(inj_config) = LanguageRegistry::singleton().language(&inj_language) {
271                match Query::new(&inj_config.language, &inj_config.highlights) {
272                    Ok(q) => {
273                        injection_queries.insert(inj_config.name.clone(), q);
274                    }
275                    Err(e) => {
276                        tracing::error!(
277                            "failed to build injection query for {:?}: {:?}",
278                            inj_config.name,
279                            e
280                        );
281                    }
282                }
283            }
284        }
285
286        // let highlight_indices = vec![None; query.capture_names().len()];
287
288        Ok(Self {
289            language: config.name.clone(),
290            query: Some(query),
291            injection_queries,
292
293            locals_pattern_index,
294            highlights_pattern_index,
295            non_local_variable_patterns,
296            injection_content_capture_index,
297            injection_language_capture_index,
298            local_scope_capture_index,
299            local_def_capture_index,
300            local_def_value_capture_index,
301            local_ref_capture_index,
302            text: Rope::new(),
303            parser,
304            tree: None,
305        })
306    }
307
308    pub fn is_empty(&self) -> bool {
309        self.text.len() == 0
310    }
311
312    /// Highlight the given text, returning a map from byte ranges to highlight captures.
313    ///
314    /// Uses incremental parsing by `edit` to efficiently update the highlighter's state.
315    pub fn update(&mut self, edit: Option<InputEdit>, text: &Rope) {
316        if self.text.eq(text) {
317            return;
318        }
319
320        let edit = edit.unwrap_or(InputEdit {
321            start_byte: 0,
322            old_end_byte: 0,
323            new_end_byte: text.len(),
324            start_position: Point::new(0, 0),
325            old_end_position: Point::new(0, 0),
326            new_end_position: Point::new(0, 0),
327        });
328
329        let mut old_tree = self
330            .tree
331            .take()
332            .unwrap_or(self.parser.parse("", None).unwrap());
333        old_tree.edit(&edit);
334
335        let new_tree = self.parser.parse_with_options(
336            &mut move |offset, _| {
337                if offset >= text.len() {
338                    ""
339                } else {
340                    let (chunk, chunk_byte_ix) = text.chunk(offset);
341                    &chunk[offset - chunk_byte_ix..]
342                }
343            },
344            Some(&old_tree),
345            None,
346        );
347
348        let Some(new_tree) = new_tree else {
349            return;
350        };
351
352        self.tree = Some(new_tree);
353        self.text = text.clone();
354    }
355
356    /// Match the visible ranges of nodes in the Tree for highlighting.
357    fn match_styles(&self, range: Range<usize>) -> Vec<HighlightItem> {
358        let mut highlights = vec![];
359        let Some(tree) = &self.tree else {
360            return highlights;
361        };
362
363        let Some(query) = &self.query else {
364            return highlights;
365        };
366
367        let root_node = tree.root_node();
368
369        let source = &self.text;
370        let mut cursor = QueryCursor::new();
371        cursor.set_byte_range(range);
372        let mut matches = cursor.matches(&query, root_node, TextProvider(&source));
373
374        while let Some(query_match) = matches.next() {
375            // Ref:
376            // https://github.com/tree-sitter/tree-sitter/blob/460118b4c82318b083b4d527c9c750426730f9c0/highlight/src/lib.rs#L556
377            if let (Some(language_name), Some(content_node), _) =
378                self.injection_for_match(None, query, query_match)
379            {
380                let styles = self.handle_injection(&language_name, content_node);
381                for (node_range, highlight_name) in styles {
382                    highlights.push(HighlightItem::new(node_range.clone(), highlight_name));
383                }
384
385                continue;
386            }
387
388            for cap in query_match.captures {
389                let node = cap.node;
390
391                let Some(highlight_name) = query.capture_names().get(cap.index as usize) else {
392                    continue;
393                };
394
395                let node_range: Range<usize> = node.start_byte()..node.end_byte();
396                let highlight_name = SharedString::from(highlight_name.to_string());
397
398                // Merge near range and same highlight name
399                let last_item = highlights.last();
400                let last_range = last_item.map(|item| &item.range).unwrap_or(&(0..0));
401                let last_highlight_name = last_item.map(|item| item.name.clone());
402
403                if last_range.end <= node_range.start
404                    && last_highlight_name.as_ref() == Some(&highlight_name)
405                {
406                    highlights.push(HighlightItem::new(
407                        last_range.start..node_range.end,
408                        highlight_name.clone(),
409                    ));
410                } else if last_range == &node_range {
411                    // case:
412                    // last_range: 213..220, last_highlight_name: Some("property")
413                    // last_range: 213..220, last_highlight_name: Some("string")
414                    highlights.push(HighlightItem::new(
415                        node_range,
416                        last_highlight_name.unwrap_or(highlight_name),
417                    ));
418                } else {
419                    highlights.push(HighlightItem::new(node_range, highlight_name.clone()));
420                }
421            }
422        }
423
424        // DO NOT REMOVE THIS PRINT, it's useful for debugging
425        // for item in highlights {
426        //     println!("item: {:?}", item);
427        // }
428
429        highlights
430    }
431
432    /// TODO: Use incremental parsing to handle the injection.
433    fn handle_injection(
434        &self,
435        injection_language: &str,
436        node: Node,
437    ) -> Vec<(Range<usize>, String)> {
438        // Ensure byte offsets are on char boundaries for UTF-8 safety
439        let start_offset = self.text.clip_offset(node.start_byte(), Bias::Left);
440        let end_offset = self.text.clip_offset(node.end_byte(), Bias::Right);
441
442        let mut cache = vec![];
443        let Some(query) = &self.injection_queries.get(injection_language) else {
444            return cache;
445        };
446
447        let content = self.text.slice(start_offset..end_offset);
448        if content.len() == 0 {
449            return cache;
450        };
451        // FIXME: Avoid to_string.
452        let content = content.to_string();
453
454        let Some(config) = LanguageRegistry::singleton().language(injection_language) else {
455            return cache;
456        };
457        let mut parser = Parser::new();
458        if parser.set_language(&config.language).is_err() {
459            return cache;
460        }
461
462        let source = content.as_bytes();
463        let Some(tree) = parser.parse(source, None) else {
464            return cache;
465        };
466
467        let mut query_cursor = QueryCursor::new();
468        let mut matches = query_cursor.matches(query, tree.root_node(), source);
469
470        let mut last_end = start_offset;
471        while let Some(m) = matches.next() {
472            for cap in m.captures {
473                let cap_node = cap.node;
474
475                let node_range: Range<usize> =
476                    start_offset + cap_node.start_byte()..start_offset + cap_node.end_byte();
477
478                if node_range.start < last_end {
479                    continue;
480                }
481                if node_range.end > end_offset {
482                    break;
483                }
484
485                if let Some(highlight_name) = query.capture_names().get(cap.index as usize) {
486                    last_end = node_range.end;
487                    cache.push((node_range, highlight_name.to_string()));
488                }
489            }
490        }
491
492        cache
493    }
494
495    /// Ref:
496    /// https://github.com/tree-sitter/tree-sitter/blob/v0.25.5/highlight/src/lib.rs#L1229
497    ///
498    /// Returns:
499    /// - `language_name`: The language name of the injection.
500    /// - `content_node`: The content node of the injection.
501    /// - `include_children`: Whether to include the children of the content node.
502    fn injection_for_match<'a>(
503        &self,
504        parent_name: Option<SharedString>,
505        query: &'a Query,
506        query_match: &QueryMatch<'a, 'a>,
507    ) -> (Option<SharedString>, Option<Node<'a>>, bool) {
508        let content_capture_index = self.injection_content_capture_index;
509        // let language_capture_index = self.injection_language_capture_index;
510
511        let mut language_name: Option<SharedString> = None;
512        let mut content_node = None;
513
514        for capture in query_match.captures {
515            let index = Some(capture.index);
516            if index == content_capture_index {
517                content_node = Some(capture.node);
518            }
519        }
520
521        let mut include_children = false;
522        for prop in query.property_settings(query_match.pattern_index) {
523            match prop.key.as_ref() {
524                // In addition to specifying the language name via the text of a
525                // captured node, it can also be hard-coded via a `#set!` predicate
526                // that sets the injection.language key.
527                "injection.language" => {
528                    if language_name.is_none() {
529                        language_name = prop
530                            .value
531                            .as_ref()
532                            .map(std::convert::AsRef::as_ref)
533                            .map(ToString::to_string)
534                            .map(SharedString::from);
535                    }
536                }
537
538                // Setting the `injection.self` key can be used to specify that the
539                // language name should be the same as the language of the current
540                // layer.
541                "injection.self" => {
542                    if language_name.is_none() {
543                        language_name = Some(self.language.clone());
544                    }
545                }
546
547                // Setting the `injection.parent` key can be used to specify that
548                // the language name should be the same as the language of the
549                // parent layer
550                "injection.parent" => {
551                    if language_name.is_none() {
552                        language_name = parent_name.clone();
553                    }
554                }
555
556                // By default, injections do not include the *children* of an
557                // `injection.content` node - only the ranges that belong to the
558                // node itself. This can be changed using a `#set!` predicate that
559                // sets the `injection.include-children` key.
560                "injection.include-children" => include_children = true,
561                _ => {}
562            }
563        }
564
565        (language_name, content_node, include_children)
566    }
567
568    /// The argument `range` is the range of the line in the text.
569    ///
570    /// Returns `range` is the range in the line.
571    pub(crate) fn styles(
572        &self,
573        range: &Range<usize>,
574        theme: &HighlightTheme,
575    ) -> Vec<(Range<usize>, HighlightStyle)> {
576        let mut styles = vec![];
577        let start_offset = range.start;
578
579        let highlights = self.match_styles(range.clone());
580
581        // let mut iter_count = 0;
582        for item in highlights {
583            // iter_count += 1;
584            let node_range = &item.range;
585            let name = &item.name;
586
587            // Avoid start larger than end
588            let mut node_range = node_range.start.max(range.start)..node_range.end.min(range.end);
589            if node_range.start > node_range.end {
590                node_range.end = node_range.start;
591            }
592
593            styles.push((node_range, theme.style(name.as_ref()).unwrap_or_default()));
594        }
595
596        // If the matched styles is empty, return a default range.
597        if styles.len() == 0 {
598            return vec![(start_offset..range.end, HighlightStyle::default())];
599        }
600
601        let styles = unique_styles(&range, styles);
602
603        // NOTE: DO NOT remove this comment, it is used for debugging.
604        // for style in &styles {
605        //     println!("---- style: {:?} - {:?}", style.0, style.1.color);
606        // }
607        // println!("--------------------------------");
608
609        styles
610    }
611}
612
613/// To merge intersection ranges, let the subsequent range cover
614/// the previous overlapping range and split the previous range.
615///
616/// From:
617///
618/// AA
619///   BBB
620///    CCCCC
621///      DD
622///         EEEE
623///
624/// To:
625///
626/// AABCCDDCEEEE
627pub(crate) fn unique_styles(
628    total_range: &Range<usize>,
629    styles: Vec<(Range<usize>, HighlightStyle)>,
630) -> Vec<(Range<usize>, HighlightStyle)> {
631    if styles.is_empty() {
632        return styles;
633    }
634
635    let mut intervals = BTreeSet::new();
636    let mut significant_intervals = BTreeSet::new();
637
638    // For example
639    //
640    // from: [(6..11), (6..11), (11..17), (17..25), (16..19), (25..59))]
641    // to:   [6, 11, 16, 17, 19, 25, 59]
642    intervals.insert(total_range.start);
643    intervals.insert(total_range.end);
644    for (range, _) in &styles {
645        intervals.insert(range.start);
646        intervals.insert(range.end);
647        significant_intervals.insert(range.end); // End points are significant for merging decisions
648    }
649
650    let intervals: Vec<usize> = intervals.into_iter().collect();
651    let mut result = Vec::with_capacity(intervals.len().saturating_sub(1));
652
653    // For each interval between boundaries, find the top-most style
654    //
655    // Result e.g.:
656    //
657    // [(6..11, red), (11..16, green), (16..17, blue), (17..19, red), (19..25, clean), (25..59, blue)]
658    for i in 0..intervals.len().saturating_sub(1) {
659        let interval = intervals[i]..intervals[i + 1];
660        if interval.start >= interval.end {
661            continue;
662        }
663
664        // Find the last (top-most) style that covers this interval
665        let mut top_style: Option<HighlightStyle> = None;
666        for (range, style) in &styles {
667            if range.start <= interval.start && interval.end <= range.end {
668                if let Some(top_style) = &mut top_style {
669                    merge_highlight_style(top_style, style);
670                } else {
671                    top_style = Some(*style);
672                }
673            }
674        }
675
676        if let Some(style) = top_style {
677            result.push((interval, style));
678        } else {
679            result.push((interval, HighlightStyle::default()));
680        }
681    }
682
683    // Merge adjacent ranges with the same style, but not across significant boundaries
684    let mut merged: Vec<(Range<usize>, HighlightStyle)> = Vec::with_capacity(result.len());
685    for (range, style) in result {
686        if let Some((last_range, last_style)) = merged.last_mut() {
687            if last_range.end == range.start
688                && *last_style == style
689                && !significant_intervals.contains(&range.start)
690            {
691                // Merge adjacent ranges with same style, but not across significant boundaries
692                last_range.end = range.end;
693                continue;
694            }
695        }
696        merged.push((range, style));
697    }
698
699    merged
700}
701
702/// Merge other style (Other on top)
703fn merge_highlight_style(style: &mut HighlightStyle, other: &HighlightStyle) {
704    if let Some(color) = other.color {
705        style.color = Some(color);
706    }
707    if let Some(font_weight) = other.font_weight {
708        style.font_weight = Some(font_weight);
709    }
710    if let Some(font_style) = other.font_style {
711        style.font_style = Some(font_style);
712    }
713    if let Some(background_color) = other.background_color {
714        style.background_color = Some(background_color);
715    }
716    if let Some(underline) = other.underline {
717        style.underline = Some(underline);
718    }
719    if let Some(strikethrough) = other.strikethrough {
720        style.strikethrough = Some(strikethrough);
721    }
722    if let Some(fade_out) = other.fade_out {
723        style.fade_out = Some(fade_out);
724    }
725}
726
727#[cfg(test)]
728mod tests {
729    use gpui::Hsla;
730
731    use super::*;
732    use crate::Colorize as _;
733
734    fn color_style(color: Hsla) -> HighlightStyle {
735        let mut style = HighlightStyle::default();
736        style.color = Some(color);
737        style
738    }
739
740    #[track_caller]
741    fn assert_unique_styles(
742        range: Range<usize>,
743        left: Vec<(Range<usize>, HighlightStyle)>,
744        right: Vec<(Range<usize>, HighlightStyle)>,
745    ) {
746        fn color_name(c: Option<Hsla>) -> String {
747            match c {
748                Some(c) => {
749                    if c == gpui::red() {
750                        "red".to_string()
751                    } else if c == gpui::green() {
752                        "green".to_string()
753                    } else if c == gpui::blue() {
754                        "blue".to_string()
755                    } else {
756                        c.to_hex()
757                    }
758                }
759                None => "clean".to_string(),
760            }
761        }
762
763        let left = unique_styles(&range, left);
764        if left.len() != right.len() {
765            println!("\n---------------------------------------------");
766            for (range, style) in left.iter() {
767                println!("({:?}, {})", range, color_name(style.color));
768            }
769            println!("---------------------------------------------");
770            panic!("left {} styles, right {} styles", left.len(), right.len());
771        }
772        for (left, right) in left.into_iter().zip(right) {
773            if left.1.color != right.1.color || left.0 != right.0 {
774                panic!(
775                    "\n left: ({:?}, {})\nright: ({:?}, {})\n",
776                    left.0,
777                    color_name(left.1.color),
778                    right.0,
779                    color_name(right.1.color)
780                );
781            }
782        }
783    }
784
785    #[test]
786    fn test_unique_styles() {
787        let red = color_style(gpui::red());
788        let green = color_style(gpui::green());
789        let blue = color_style(gpui::blue());
790        let clean = HighlightStyle::default();
791
792        assert_unique_styles(
793            0..65,
794            vec![
795                (2..10, clean),
796                (2..10, clean),
797                (5..11, red),
798                (2..6, clean),
799                (10..15, green),
800                (15..30, clean),
801                (29..35, blue),
802                (35..40, green),
803                (45..60, blue),
804            ],
805            vec![
806                (0..5, clean),
807                (5..6, red),
808                (6..10, red),
809                (10..11, green),
810                (11..15, green),
811                (15..29, clean),
812                (29..30, blue),
813                (30..35, blue),
814                (35..40, green),
815                (40..45, clean),
816                (45..60, blue),
817                (60..65, clean),
818            ],
819        );
820    }
821}