Skip to main content

binocular/preview/rich_text/
syntax.rs

1//! Syntax highlighting using tree-sitter (primary) and syntect (fallback).
2
3use ratatui::style::{Color, Modifier, Style};
4use std::collections::{BTreeMap, HashMap};
5use std::sync::{Arc, OnceLock, RwLock};
6use syntect::highlighting::ThemeSet;
7use syntect::parsing::SyntaxSet;
8use tree_sitter_highlight::{HighlightConfiguration, Highlighter};
9
10static HIGHLIGHTER: OnceLock<RwLock<Highlighter>> = OnceLock::new();
11static CONFIGS: OnceLock<BTreeMap<String, Arc<HighlightConfiguration>>> = OnceLock::new();
12static REGISTRY: OnceLock<SyntaxRegistry> = OnceLock::new();
13static SYNTAX_SET: OnceLock<SyntaxSet> = OnceLock::new();
14static THEME_SET: OnceLock<ThemeSet> = OnceLock::new();
15
16const LANGUAGE_KEYS: [&str; 13] = [
17    "rust",
18    "python",
19    "javascript",
20    "typescript",
21    "json",
22    "toml",
23    "yaml",
24    "html",
25    "css",
26    "c",
27    "cpp",
28    "go",
29    "csharp",
30];
31
32pub fn get_highlighter() -> &'static RwLock<Highlighter> {
33    HIGHLIGHTER.get_or_init(|| RwLock::new(Highlighter::new()))
34}
35
36pub fn get_configs() -> &'static BTreeMap<String, Arc<HighlightConfiguration>> {
37    CONFIGS.get_or_init(|| {
38        let mut map = BTreeMap::new();
39        add_highlight_config(
40            &mut map,
41            "rust",
42            tree_sitter_rust::LANGUAGE.into(),
43            tree_sitter_rust::HIGHLIGHTS_QUERY,
44        );
45        add_highlight_config(
46            &mut map,
47            "python",
48            tree_sitter_python::LANGUAGE.into(),
49            tree_sitter_python::HIGHLIGHTS_QUERY,
50        );
51        add_highlight_config(
52            &mut map,
53            "javascript",
54            tree_sitter_javascript::LANGUAGE.into(),
55            tree_sitter_javascript::HIGHLIGHT_QUERY,
56        );
57        add_highlight_config(
58            &mut map,
59            "typescript",
60            tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
61            tree_sitter_typescript::HIGHLIGHTS_QUERY,
62        );
63        add_highlight_config(
64            &mut map,
65            "json",
66            tree_sitter_json::LANGUAGE.into(),
67            tree_sitter_json::HIGHLIGHTS_QUERY,
68        );
69        add_highlight_config(
70            &mut map,
71            "toml",
72            tree_sitter_toml_ng::LANGUAGE.into(),
73            tree_sitter_toml_ng::HIGHLIGHTS_QUERY,
74        );
75        add_highlight_config(
76            &mut map,
77            "yaml",
78            tree_sitter_yaml::LANGUAGE.into(),
79            tree_sitter_yaml::HIGHLIGHTS_QUERY,
80        );
81        add_highlight_config(
82            &mut map,
83            "html",
84            tree_sitter_html::LANGUAGE.into(),
85            tree_sitter_html::HIGHLIGHTS_QUERY,
86        );
87        add_highlight_config(
88            &mut map,
89            "css",
90            tree_sitter_css::LANGUAGE.into(),
91            tree_sitter_css::HIGHLIGHTS_QUERY,
92        );
93        add_highlight_config(
94            &mut map,
95            "c",
96            tree_sitter_c::LANGUAGE.into(),
97            tree_sitter_c::HIGHLIGHT_QUERY,
98        );
99        add_highlight_config(
100            &mut map,
101            "cpp",
102            tree_sitter_cpp::LANGUAGE.into(),
103            tree_sitter_cpp::HIGHLIGHT_QUERY,
104        );
105        add_highlight_config(
106            &mut map,
107            "go",
108            tree_sitter_go::LANGUAGE.into(),
109            tree_sitter_go::HIGHLIGHTS_QUERY,
110        );
111        add_highlight_config(
112            &mut map,
113            "csharp",
114            tree_sitter_c_sharp::LANGUAGE.into(),
115            include_str!("../../../queries/csharp-highlights.scm"),
116        );
117
118        map
119    })
120}
121
122pub const HIGHLIGHT_NAMES: [&str; 25] = [
123    "attribute",
124    "constant",
125    "function.builtin",
126    "function",
127    "keyword",
128    "operator",
129    "property",
130    "punctuation",
131    "punctuation.bracket",
132    "punctuation.delimiter",
133    "string",
134    "string.special",
135    "tag",
136    "type",
137    "type.builtin",
138    "variable",
139    "variable.builtin",
140    "variable.parameter",
141    "comment",
142    "constructor",
143    "label",
144    "namespace",
145    "number",
146    "escape",
147    "embedded",
148];
149
150pub fn get_style(highlight_idx: usize) -> Style {
151    let name = HIGHLIGHT_NAMES.get(highlight_idx).unwrap_or(&"");
152    style_for_capture(name)
153}
154
155fn style_for_capture(name: &str) -> Style {
156    match name {
157        "attribute" => Style::default().fg(Color::Cyan),
158        "constant" => Style::default().fg(Color::Red),
159        "function.builtin" => Style::default().fg(Color::LightBlue),
160        "function" => Style::default().fg(Color::Blue),
161        "keyword" => Style::default().fg(Color::Magenta),
162        "operator" => Style::default().fg(Color::White),
163        "property" => Style::default().fg(Color::LightCyan),
164        "punctuation" => Style::default().fg(Color::DarkGray),
165        "punctuation.bracket" => Style::default().fg(Color::DarkGray),
166        "punctuation.delimiter" => Style::default().fg(Color::DarkGray),
167        "string" => Style::default().fg(Color::Green),
168        "string.special" => Style::default().fg(Color::Green),
169        "tag" => Style::default().fg(Color::LightRed),
170        "type" => Style::default().fg(Color::Yellow),
171        "type.builtin" => Style::default().fg(Color::Yellow),
172        "variable" => Style::default().fg(Color::White),
173        "variable.builtin" => Style::default().fg(Color::Red),
174        "variable.parameter" => Style::default().fg(Color::LightRed),
175        "comment" => Style::default()
176            .fg(Color::Gray)
177            .add_modifier(Modifier::ITALIC),
178        "constructor" => Style::default().fg(Color::Yellow),
179        "label" => Style::default().fg(Color::LightGreen),
180        "namespace" => Style::default().fg(Color::Yellow),
181        "number" => Style::default().fg(Color::Red),
182        "escape" => Style::default().fg(Color::Magenta),
183        "embedded" => Style::default(),
184        _ => Style::default(),
185    }
186}
187
188pub fn detect_language(path: &std::path::Path) -> Option<&'static str> {
189    let ext = path.extension()?.to_str()?;
190    detect_language_from_extension(ext)
191}
192
193fn detect_language_from_extension(ext: &str) -> Option<&'static str> {
194    if ext.eq_ignore_ascii_case("rs") {
195        return Some("rust");
196    }
197    if ext.eq_ignore_ascii_case("py") {
198        return Some("python");
199    }
200    if ext.eq_ignore_ascii_case("js") || ext.eq_ignore_ascii_case("jsx") {
201        return Some("javascript");
202    }
203    if ext.eq_ignore_ascii_case("ts") || ext.eq_ignore_ascii_case("tsx") {
204        return Some("typescript");
205    }
206    if ext.eq_ignore_ascii_case("json") {
207        return Some("json");
208    }
209    if ext.eq_ignore_ascii_case("toml") {
210        return Some("toml");
211    }
212    if ext.eq_ignore_ascii_case("yaml") || ext.eq_ignore_ascii_case("yml") {
213        return Some("yaml");
214    }
215    if ext.eq_ignore_ascii_case("html") {
216        return Some("html");
217    }
218    if ext.eq_ignore_ascii_case("css") {
219        return Some("css");
220    }
221    if ext.eq_ignore_ascii_case("c") || ext.eq_ignore_ascii_case("h") {
222        return Some("c");
223    }
224    if ext.eq_ignore_ascii_case("cpp")
225        || ext.eq_ignore_ascii_case("cc")
226        || ext.eq_ignore_ascii_case("cxx")
227        || ext.eq_ignore_ascii_case("hpp")
228    {
229        return Some("cpp");
230    }
231    if ext.eq_ignore_ascii_case("go") {
232        return Some("go");
233    }
234    if ext.eq_ignore_ascii_case("cs") {
235        return Some("csharp");
236    }
237    None
238}
239
240pub struct SyntaxRegistry {
241    languages: HashMap<&'static str, tree_sitter::Language>,
242}
243
244impl SyntaxRegistry {
245    pub fn instance() -> &'static SyntaxRegistry {
246        REGISTRY.get_or_init(Self::new)
247    }
248
249    fn new() -> Self {
250        let mut languages = HashMap::new();
251        for lang in LANGUAGE_KEYS {
252            if let Some(language) = language_from_key(lang) {
253                languages.insert(lang, language);
254            }
255        }
256
257        Self { languages }
258    }
259
260    pub fn get_language(&self, lang_name: &str) -> Option<tree_sitter::Language> {
261        self.languages.get(lang_name).cloned()
262    }
263}
264
265fn add_highlight_config(
266    map: &mut BTreeMap<String, Arc<HighlightConfiguration>>,
267    name: &str,
268    language: tree_sitter::Language,
269    query: &str,
270) {
271    if let Ok(mut config) = HighlightConfiguration::new(language, "utf-8", query, "", "") {
272        config.configure(&HIGHLIGHT_NAMES);
273        map.insert(name.to_string(), Arc::new(config));
274    }
275}
276
277pub fn get_syntax_set() -> &'static SyntaxSet {
278    SYNTAX_SET.get_or_init(SyntaxSet::load_defaults_newlines)
279}
280
281pub fn get_theme_set() -> &'static ThemeSet {
282    THEME_SET.get_or_init(ThemeSet::load_defaults)
283}
284
285fn language_from_key(name: &str) -> Option<tree_sitter::Language> {
286    match name {
287        "rust" => Some(tree_sitter::Language::from(tree_sitter_rust::LANGUAGE)),
288        "python" => Some(tree_sitter::Language::from(tree_sitter_python::LANGUAGE)),
289        "javascript" => Some(tree_sitter::Language::from(
290            tree_sitter_javascript::LANGUAGE,
291        )),
292        "typescript" => Some(tree_sitter::Language::from(
293            tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
294        )),
295        "json" => Some(tree_sitter::Language::from(tree_sitter_json::LANGUAGE)),
296        "toml" => Some(tree_sitter::Language::from(tree_sitter_toml_ng::LANGUAGE)),
297        "yaml" => Some(tree_sitter::Language::from(tree_sitter_yaml::LANGUAGE)),
298        "html" => Some(tree_sitter::Language::from(tree_sitter_html::LANGUAGE)),
299        "css" => Some(tree_sitter::Language::from(tree_sitter_css::LANGUAGE)),
300        "c" => Some(tree_sitter::Language::from(tree_sitter_c::LANGUAGE)),
301        "cpp" => Some(tree_sitter::Language::from(tree_sitter_cpp::LANGUAGE)),
302        "go" => Some(tree_sitter::Language::from(tree_sitter_go::LANGUAGE)),
303        "csharp" => Some(tree_sitter::Language::from(tree_sitter_c_sharp::LANGUAGE)),
304        _ => None,
305    }
306}