binocular/preview/rich_text/
syntax.rs1use 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}