Skip to main content

compose_rust/
highlight.rs

1//! From a highlighter's names to the code colour roles.
2//!
3//! A highlighter, tree-sitter or anything that speaks its vocabulary, names each token with
4//! a dotted capture name such as `keyword.return` or `variable.parameter`. The Renderer does
5//! not know any language and never hears these names: they are turned into colour roles
6//! here, on the Host side, and the roles travel as the `Paint` of a span the way any other
7//! coloured run does. Markdown is handled the same way, for the same reason.
8//!
9//! The names are matched from the most specific prefix down, so `string.escape` is an
10//! escape while `string.special` is still a string, and a name nothing here recognises gives
11//! `None`. A token without a role is left in the body ink rather than painted a guess.
12
13use crate::schema::{ColorRole, Paint};
14use crate::spans::TextSpan;
15
16/// The colour role a highlight name is painted with, or None to leave it in the body ink.
17///
18/// Accepts tree-sitter's capture names (`keyword.control`, `function.method`,
19/// `punctuation.bracket`, `diff.plus`), the older names some grammars still use
20/// (`conditional`, `repeat`, `include`, `float`, `field`, `preproc`), and VS Code's
21/// semantic token kinds (`enumMember`, `typeParameter`, `decorator`).
22pub fn role_for_highlight(name: &str) -> Option<ColorRole> {
23    // Longest first: every row is a prefix that ends at a dot or at the end of the name.
24    const TABLE: &[(&str, ColorRole)] = &[
25        ("string.escape", ColorRole::SyntaxEscape),
26        ("string.regexp", ColorRole::SyntaxEscape),
27        ("string.regex", ColorRole::SyntaxEscape),
28        ("constant.character.escape", ColorRole::SyntaxEscape),
29        ("function.macro", ColorRole::SyntaxMacro),
30        ("keyword.directive", ColorRole::SyntaxMacro),
31        ("keyword.import", ColorRole::SyntaxKeyword),
32        ("variable.member", ColorRole::SyntaxProperty),
33        ("variable.other.member", ColorRole::SyntaxProperty),
34        ("tag.attribute", ColorRole::SyntaxAttribute),
35        ("constructor", ColorRole::SyntaxFunction),
36        ("keyword", ColorRole::SyntaxKeyword),
37        ("conditional", ColorRole::SyntaxKeyword),
38        ("repeat", ColorRole::SyntaxKeyword),
39        ("exception", ColorRole::SyntaxKeyword),
40        ("include", ColorRole::SyntaxKeyword),
41        ("storageclass", ColorRole::SyntaxKeyword),
42        ("modifier", ColorRole::SyntaxKeyword),
43        ("string", ColorRole::SyntaxString),
44        ("character", ColorRole::SyntaxString),
45        ("comment", ColorRole::SyntaxComment),
46        ("number", ColorRole::SyntaxNumber),
47        ("float", ColorRole::SyntaxNumber),
48        ("boolean", ColorRole::SyntaxConstant),
49        ("constant", ColorRole::SyntaxConstant),
50        ("enumMember", ColorRole::SyntaxConstant),
51        ("type", ColorRole::SyntaxType),
52        ("typeParameter", ColorRole::SyntaxType),
53        ("class", ColorRole::SyntaxType),
54        ("struct", ColorRole::SyntaxType),
55        ("enum", ColorRole::SyntaxType),
56        ("interface", ColorRole::SyntaxType),
57        ("namespace", ColorRole::SyntaxType),
58        ("module", ColorRole::SyntaxType),
59        ("function", ColorRole::SyntaxFunction),
60        ("method", ColorRole::SyntaxFunction),
61        ("variable", ColorRole::SyntaxVariable),
62        ("parameter", ColorRole::SyntaxVariable),
63        ("property", ColorRole::SyntaxProperty),
64        ("field", ColorRole::SyntaxProperty),
65        ("operator", ColorRole::SyntaxOperator),
66        ("punctuation", ColorRole::SyntaxPunctuation),
67        ("tag", ColorRole::SyntaxTag),
68        ("attribute", ColorRole::SyntaxAttribute),
69        ("macro", ColorRole::SyntaxMacro),
70        ("preproc", ColorRole::SyntaxMacro),
71        ("define", ColorRole::SyntaxMacro),
72        ("decorator", ColorRole::SyntaxMacro),
73        ("annotation", ColorRole::SyntaxMacro),
74        ("regexp", ColorRole::SyntaxEscape),
75        ("diff.plus", ColorRole::DiffAdded),
76        ("diff.minus", ColorRole::DiffRemoved),
77        ("diff.delta", ColorRole::DiffModified),
78    ];
79    let matches = |prefix: &str| {
80        name.strip_prefix(prefix)
81            .is_some_and(|rest| rest.is_empty() || rest.starts_with('.'))
82    };
83    TABLE
84        .iter()
85        .filter(|(prefix, _)| matches(prefix))
86        .max_by_key(|(prefix, _)| prefix.len())
87        .map(|(_, role)| *role)
88}
89
90/// A span painting `length` bytes from `start` in the role a highlight name maps to, or
91/// None where the name has no role and the run is left in the body ink.
92pub fn highlight_span(start: u32, length: u32, name: &str) -> Option<TextSpan> {
93    role_for_highlight(name).map(|role| TextSpan::new(start, length).with_color(Paint::Role(role)))
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    /// The finer names fold into the twenty two, and the most specific prefix wins.
101    #[test]
102    fn fr13_1_3_highlight_names_fold_into_the_code_roles() {
103        for (name, role) in [
104            ("keyword", ColorRole::SyntaxKeyword),
105            ("keyword.return", ColorRole::SyntaxKeyword),
106            ("keyword.directive", ColorRole::SyntaxMacro),
107            ("string", ColorRole::SyntaxString),
108            ("string.special", ColorRole::SyntaxString),
109            ("string.escape", ColorRole::SyntaxEscape),
110            ("string.regexp", ColorRole::SyntaxEscape),
111            ("character", ColorRole::SyntaxString),
112            ("comment.documentation", ColorRole::SyntaxComment),
113            ("number", ColorRole::SyntaxNumber),
114            ("float", ColorRole::SyntaxNumber),
115            ("constant.builtin", ColorRole::SyntaxConstant),
116            ("boolean", ColorRole::SyntaxConstant),
117            ("type.builtin", ColorRole::SyntaxType),
118            ("function.method", ColorRole::SyntaxFunction),
119            ("function.macro", ColorRole::SyntaxMacro),
120            ("constructor", ColorRole::SyntaxFunction),
121            ("variable.parameter", ColorRole::SyntaxVariable),
122            ("variable.member", ColorRole::SyntaxProperty),
123            ("property", ColorRole::SyntaxProperty),
124            ("field", ColorRole::SyntaxProperty),
125            ("operator", ColorRole::SyntaxOperator),
126            ("punctuation.bracket", ColorRole::SyntaxPunctuation),
127            ("tag", ColorRole::SyntaxTag),
128            ("attribute", ColorRole::SyntaxAttribute),
129            ("preproc", ColorRole::SyntaxMacro),
130            ("diff.plus", ColorRole::DiffAdded),
131            ("diff.minus", ColorRole::DiffRemoved),
132            ("diff.delta", ColorRole::DiffModified),
133        ] {
134            assert_eq!(role_for_highlight(name), Some(role), "{name}");
135        }
136    }
137
138    /// A name nothing here knows stays in the body ink, and a prefix only counts at a dot.
139    #[test]
140    fn fr13_1_3_an_unknown_name_is_left_unpainted() {
141        assert_eq!(role_for_highlight("embedded"), None);
142        assert_eq!(role_for_highlight("spell"), None);
143        assert_eq!(role_for_highlight("keywords"), None);
144        assert!(highlight_span(0, 3, "none").is_none());
145    }
146}