1use std::path::Path;
2
3pub use tree_sitter;
5pub use tree_sitter_highlight;
6pub use tree_sitter_highlight::HighlightConfiguration;
7
8#[cfg(feature = "tree-sitter-go")]
12pub use tree_sitter_go;
13#[cfg(feature = "tree-sitter-javascript")]
14pub use tree_sitter_javascript;
15#[cfg(feature = "tree-sitter-json")]
16pub use tree_sitter_json;
17#[cfg(feature = "tree-sitter-templ")]
18pub use tree_sitter_templ;
19#[cfg(feature = "tree-sitter-typescript")]
20pub use tree_sitter_typescript;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum HighlightCategory {
25 Attribute,
26 Comment,
27 Constant,
28 Function,
29 Keyword,
30 Number,
31 Operator,
32 PunctuationBracket,
33 PunctuationDelimiter,
34 Property,
35 String,
36 Type,
37 Variable,
38 VariableBuiltin,
39 Inserted,
44 Deleted,
47 Changed,
51}
52
53impl HighlightCategory {
54 pub fn bg_extends_to_line_end(&self) -> bool {
62 matches!(self, Self::Inserted | Self::Deleted | Self::Changed)
63 }
64
65 pub fn from_default_index(index: usize) -> Option<Self> {
67 match index {
68 0 => Some(Self::Attribute),
69 1 => Some(Self::Comment),
70 2 => Some(Self::Constant),
71 3 => Some(Self::Function),
72 4 => Some(Self::Keyword),
73 5 => Some(Self::Number),
74 6 => Some(Self::Operator),
75 7 => Some(Self::PunctuationBracket),
76 8 => Some(Self::PunctuationDelimiter),
77 9 => Some(Self::Property),
78 10 => Some(Self::String),
79 11 => Some(Self::Type),
80 12 => Some(Self::Variable),
81 13 => Some(Self::VariableBuiltin),
82 _ => None,
83 }
84 }
85
86 pub fn from_typescript_index(index: usize) -> Option<Self> {
88 match index {
89 0 => Some(Self::Attribute), 1 => Some(Self::Comment), 2 => Some(Self::Constant), 3 => Some(Self::Constant), 4 => Some(Self::Type), 5 => Some(Self::String), 6 => Some(Self::Function), 7 => Some(Self::Function), 8 => Some(Self::Function), 9 => Some(Self::Keyword), 10 => Some(Self::Number), 11 => Some(Self::Operator), 12 => Some(Self::Property), 13 => Some(Self::PunctuationBracket), 14 => Some(Self::PunctuationDelimiter), 15 => Some(Self::Constant), 16 => Some(Self::String), 17 => Some(Self::String), 18 => Some(Self::Type), 19 => Some(Self::Type), 20 => Some(Self::Variable), 21 => Some(Self::VariableBuiltin), 22 => Some(Self::Variable), _ => None,
113 }
114 }
115
116 pub fn theme_key(&self) -> &'static str {
118 match self {
119 Self::Keyword => "syntax.keyword",
120 Self::String => "syntax.string",
121 Self::Comment => "syntax.comment",
122 Self::Function => "syntax.function",
123 Self::Type => "syntax.type",
124 Self::Variable | Self::Property => "syntax.variable",
125 Self::VariableBuiltin => "syntax.variable_builtin",
126 Self::Constant | Self::Number | Self::Attribute => "syntax.constant",
127 Self::Operator => "syntax.operator",
128 Self::PunctuationBracket => "syntax.punctuation_bracket",
129 Self::PunctuationDelimiter => "syntax.punctuation_delimiter",
130 Self::Inserted => "editor.diff_add_bg",
135 Self::Deleted => "editor.diff_remove_bg",
136 Self::Changed => "editor.diff_modify_bg",
137 }
138 }
139
140 pub fn display_name(&self) -> &'static str {
142 match self {
143 Self::Attribute => "Attribute",
144 Self::Comment => "Comment",
145 Self::Constant => "Constant",
146 Self::Function => "Function",
147 Self::Keyword => "Keyword",
148 Self::Number => "Number",
149 Self::Operator => "Operator",
150 Self::PunctuationBracket => "Punctuation Bracket",
151 Self::PunctuationDelimiter => "Punctuation Delimiter",
152 Self::Property => "Property",
153 Self::String => "String",
154 Self::Type => "Type",
155 Self::Variable => "Variable",
156 Self::VariableBuiltin => "Variable (Builtin)",
157 Self::Inserted => "Diff Inserted",
158 Self::Deleted => "Diff Deleted",
159 Self::Changed => "Diff Changed",
160 }
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
166pub enum Language {
167 Rust,
168 Python,
169 JavaScript,
170 TypeScript,
171 HTML,
172 CSS,
173 C,
174 Cpp,
175 Go,
176 Json,
177 Jsonc,
178 Java,
179 CSharp,
180 Php,
181 Ruby,
182 Bash,
183 Lua,
184 Pascal,
185 Odin,
186 Templ,
187}
188
189impl Language {
190 pub fn from_path(path: &Path) -> Option<Self> {
196 let ext = path.extension()?.to_str()?;
197 Self::all()
198 .iter()
199 .find(|lang| lang.extensions().contains(&ext))
200 .copied()
201 }
202
203 pub fn highlight_config(&self) -> Result<HighlightConfiguration, String> {
205 match self {
206 Self::JavaScript => {
207 #[cfg(feature = "tree-sitter-javascript")]
208 {
209 let mut config = HighlightConfiguration::new(
210 tree_sitter_javascript::LANGUAGE.into(),
211 "javascript",
212 tree_sitter_javascript::HIGHLIGHT_QUERY,
213 "",
214 "",
215 )
216 .map_err(|e| format!("Failed to create JavaScript highlight config: {e}"))?;
217 config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
218 Ok(config)
219 }
220 #[cfg(not(feature = "tree-sitter-javascript"))]
221 Err("JavaScript language support not enabled".to_string())
222 }
223 Self::TypeScript => {
224 #[cfg(all(feature = "tree-sitter-typescript", feature = "tree-sitter-javascript"))]
225 {
226 let combined_highlights = format!(
227 "{}\n{}",
228 tree_sitter_typescript::HIGHLIGHTS_QUERY,
229 tree_sitter_javascript::HIGHLIGHT_QUERY
230 );
231 let mut config = HighlightConfiguration::new(
232 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
233 "typescript",
234 &combined_highlights,
235 "",
236 tree_sitter_typescript::LOCALS_QUERY,
237 )
238 .map_err(|e| format!("Failed to create TypeScript highlight config: {e}"))?;
239 config.configure(TYPESCRIPT_HIGHLIGHT_CAPTURES);
240 Ok(config)
241 }
242 #[cfg(not(all(
243 feature = "tree-sitter-typescript",
244 feature = "tree-sitter-javascript"
245 )))]
246 Err("TypeScript language support not enabled".to_string())
247 }
248 Self::Go => {
249 #[cfg(feature = "tree-sitter-go")]
250 {
251 let mut config = HighlightConfiguration::new(
252 tree_sitter_go::LANGUAGE.into(),
253 "go",
254 tree_sitter_go::HIGHLIGHTS_QUERY,
255 "",
256 "",
257 )
258 .map_err(|e| format!("Failed to create Go highlight config: {e}"))?;
259 config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
260 Ok(config)
261 }
262 #[cfg(not(feature = "tree-sitter-go"))]
263 Err("Go language support not enabled".to_string())
264 }
265 Self::Json => {
266 #[cfg(feature = "tree-sitter-json")]
267 {
268 let mut config = HighlightConfiguration::new(
269 tree_sitter_json::LANGUAGE.into(),
270 "json",
271 tree_sitter_json::HIGHLIGHTS_QUERY,
272 "",
273 "",
274 )
275 .map_err(|e| format!("Failed to create JSON highlight config: {e}"))?;
276 config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
277 Ok(config)
278 }
279 #[cfg(not(feature = "tree-sitter-json"))]
280 Err("JSON language support not enabled".to_string())
281 }
282 Self::Jsonc => {
283 #[cfg(feature = "tree-sitter-json")]
288 {
289 let mut config = HighlightConfiguration::new(
290 tree_sitter_json::LANGUAGE.into(),
291 "jsonc",
292 tree_sitter_json::HIGHLIGHTS_QUERY,
293 "",
294 "",
295 )
296 .map_err(|e| format!("Failed to create JSONC highlight config: {e}"))?;
297 config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
298 Ok(config)
299 }
300 #[cfg(not(feature = "tree-sitter-json"))]
301 Err("JSONC language support not enabled".to_string())
302 }
303 Self::Templ => {
304 #[cfg(feature = "tree-sitter-templ")]
309 {
310 let combined_highlights = format!(
311 "{}\n{}",
312 tree_sitter_go::HIGHLIGHTS_QUERY,
313 TEMPL_HIGHLIGHTS_QUERY,
314 );
315 let mut config = HighlightConfiguration::new(
316 tree_sitter_templ::LANGUAGE.into(),
317 "templ",
318 &combined_highlights,
319 "",
320 "",
321 )
322 .map_err(|e| format!("Failed to create Templ highlight config: {e}"))?;
323 config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
324 Ok(config)
325 }
326 #[cfg(not(feature = "tree-sitter-templ"))]
327 Err("Templ language support not enabled".to_string())
328 }
329 _ => Err("no bundled tree-sitter grammar for this language".to_string()),
332 }
333 }
334
335 pub fn highlight_category(&self, index: usize) -> Option<HighlightCategory> {
337 match self {
338 Self::TypeScript => HighlightCategory::from_typescript_index(index),
339 _ => HighlightCategory::from_default_index(index),
340 }
341 }
342
343 pub fn ts_language(&self) -> Option<tree_sitter::Language> {
355 match self {
356 Self::JavaScript => {
357 #[cfg(feature = "tree-sitter-javascript")]
358 {
359 Some(tree_sitter_javascript::LANGUAGE.into())
360 }
361 #[cfg(not(feature = "tree-sitter-javascript"))]
362 {
363 None
364 }
365 }
366 Self::TypeScript => {
367 #[cfg(feature = "tree-sitter-typescript")]
368 {
369 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into())
370 }
371 #[cfg(not(feature = "tree-sitter-typescript"))]
372 {
373 None
374 }
375 }
376 Self::Go => {
377 #[cfg(feature = "tree-sitter-go")]
378 {
379 Some(tree_sitter_go::LANGUAGE.into())
380 }
381 #[cfg(not(feature = "tree-sitter-go"))]
382 {
383 None
384 }
385 }
386 Self::Json | Self::Jsonc => {
387 #[cfg(feature = "tree-sitter-json")]
388 {
389 Some(tree_sitter_json::LANGUAGE.into())
390 }
391 #[cfg(not(feature = "tree-sitter-json"))]
392 {
393 None
394 }
395 }
396 Self::Templ => {
397 #[cfg(feature = "tree-sitter-templ")]
398 {
399 Some(tree_sitter_templ::LANGUAGE.into())
400 }
401 #[cfg(not(feature = "tree-sitter-templ"))]
402 {
403 None
404 }
405 }
406 _ => None,
409 }
410 }
411}
412
413impl Language {
414 pub fn all() -> &'static [Language] {
416 &[
417 Language::Rust,
418 Language::Python,
419 Language::JavaScript,
420 Language::TypeScript,
421 Language::HTML,
422 Language::CSS,
423 Language::C,
424 Language::Cpp,
425 Language::Go,
426 Language::Json,
427 Language::Jsonc,
428 Language::Java,
429 Language::CSharp,
430 Language::Php,
431 Language::Ruby,
432 Language::Bash,
433 Language::Lua,
434 Language::Pascal,
435 Language::Odin,
436 Language::Templ,
437 ]
438 }
439
440 pub fn id(&self) -> &'static str {
442 match self {
443 Self::Rust => "rust",
444 Self::Python => "python",
445 Self::JavaScript => "javascript",
446 Self::TypeScript => "typescript",
447 Self::HTML => "html",
448 Self::CSS => "css",
449 Self::C => "c",
450 Self::Cpp => "cpp",
451 Self::Go => "go",
452 Self::Json => "json",
453 Self::Jsonc => "jsonc",
454 Self::Java => "java",
455 Self::CSharp => "csharp",
456 Self::Php => "php",
457 Self::Ruby => "ruby",
458 Self::Bash => "bash",
459 Self::Lua => "lua",
460 Self::Pascal => "pascal",
461 Self::Odin => "odin",
462 Self::Templ => "templ",
463 }
464 }
465
466 pub fn lsp_language_id(&self, path: &Path) -> &'static str {
474 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
475 match (self, ext) {
476 (Self::TypeScript, "tsx") => "typescriptreact",
477 (Self::JavaScript, "jsx") => "javascriptreact",
478 _ => self.id(),
479 }
480 }
481
482 pub fn extensions(&self) -> &'static [&'static str] {
488 match self {
489 Self::Rust => &["rs"],
490 Self::Python => &["py"],
491 Self::JavaScript => &["js", "jsx", "mjs", "cjs"],
492 Self::TypeScript => &["ts", "tsx", "mts", "cts"],
493 Self::HTML => &["html"],
494 Self::CSS => &["css"],
495 Self::C => &["c", "h"],
496 Self::Cpp => &["cpp", "hpp", "cc", "hh", "cxx", "hxx", "cppm", "ixx"],
497 Self::Go => &["go"],
498 Self::Json => &["json"],
499 Self::Jsonc => &["jsonc"],
500 Self::Java => &["java"],
501 Self::CSharp => &["cs"],
502 Self::Php => &["php"],
503 Self::Ruby => &["rb"],
504 Self::Bash => &["sh", "bash"],
505 Self::Lua => &["lua"],
506 Self::Pascal => &["pas", "p"],
507 Self::Odin => &["odin"],
508 Self::Templ => &["templ"],
509 }
510 }
511
512 pub fn display_name(&self) -> &'static str {
514 match self {
515 Self::Rust => "Rust",
516 Self::Python => "Python",
517 Self::JavaScript => "JavaScript",
518 Self::TypeScript => "TypeScript",
519 Self::HTML => "HTML",
520 Self::CSS => "CSS",
521 Self::C => "C",
522 Self::Cpp => "C++",
523 Self::Go => "Go",
524 Self::Json => "JSON",
525 Self::Jsonc => "JSON with Comments",
526 Self::Java => "Java",
527 Self::CSharp => "C#",
528 Self::Php => "PHP",
529 Self::Ruby => "Ruby",
530 Self::Bash => "Bash",
531 Self::Lua => "Lua",
532 Self::Pascal => "Pascal",
533 Self::Odin => "Odin",
534 Self::Templ => "Templ",
535 }
536 }
537
538 pub fn from_id(id: &str) -> Option<Self> {
540 let id_lower = id.to_lowercase();
541 match id_lower.as_str() {
542 "rust" => Some(Self::Rust),
543 "python" => Some(Self::Python),
544 "javascript" => Some(Self::JavaScript),
545 "typescript" => Some(Self::TypeScript),
546 "html" => Some(Self::HTML),
547 "css" => Some(Self::CSS),
548 "c" => Some(Self::C),
549 "cpp" | "c++" => Some(Self::Cpp),
550 "go" => Some(Self::Go),
551 "json" => Some(Self::Json),
552 "jsonc" => Some(Self::Jsonc),
553 "java" => Some(Self::Java),
554 "c_sharp" | "c#" | "csharp" => Some(Self::CSharp),
555 "php" => Some(Self::Php),
556 "ruby" => Some(Self::Ruby),
557 "bash" => Some(Self::Bash),
558 "lua" => Some(Self::Lua),
559 "pascal" => Some(Self::Pascal),
560 "odin" => Some(Self::Odin),
561 "templ" => Some(Self::Templ),
562 _ => None,
563 }
564 }
565
566 pub fn from_name(name: &str) -> Option<Self> {
575 for lang in Self::all() {
577 if lang.display_name() == name {
578 return Some(*lang);
579 }
580 }
581
582 let name_lower = name.to_lowercase();
584 match name_lower.as_str() {
585 "rust" => Some(Self::Rust),
586 "python" => Some(Self::Python),
587 "javascript" | "javascript (babel)" => Some(Self::JavaScript),
588 "typescript" | "typescriptreact" => Some(Self::TypeScript),
589 "html" => Some(Self::HTML),
590 "css" => Some(Self::CSS),
591 "c" => Some(Self::C),
592 "c++" => Some(Self::Cpp),
593 "go" | "golang" => Some(Self::Go),
594 "json" => Some(Self::Json),
595 "jsonc" | "json with comments" => Some(Self::Jsonc),
596 "java" => Some(Self::Java),
597 "c#" => Some(Self::CSharp),
598 "php" => Some(Self::Php),
599 "ruby" => Some(Self::Ruby),
600 "lua" => Some(Self::Lua),
601 "pascal" => Some(Self::Pascal),
602 "odin" => Some(Self::Odin),
603 "templ" => Some(Self::Templ),
604 _ => {
605 if name_lower.contains("bash") || name_lower.contains("shell") {
607 return Some(Self::Bash);
608 }
609 None
610 }
611 }
612 }
613}
614
615impl std::fmt::Display for Language {
616 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
617 write!(f, "{}", self.id())
618 }
619}
620
621#[allow(dead_code)]
625const DEFAULT_HIGHLIGHT_CAPTURES: &[&str] = &[
626 "attribute",
627 "comment",
628 "constant",
629 "function",
630 "keyword",
631 "number",
632 "operator",
633 "punctuation.bracket",
634 "punctuation.delimiter",
635 "property",
636 "string",
637 "type",
638 "variable",
639 "variable.builtin",
640];
641
642#[cfg(feature = "tree-sitter-templ")]
653const TEMPL_HIGHLIGHTS_QUERY: &str = include_str!("../queries/templ/highlights.scm");
654
655#[allow(dead_code)]
657const TYPESCRIPT_HIGHLIGHT_CAPTURES: &[&str] = &[
658 "attribute",
659 "comment",
660 "constant",
661 "constant.builtin",
662 "constructor",
663 "embedded",
664 "function",
665 "function.builtin",
666 "function.method",
667 "keyword",
668 "number",
669 "operator",
670 "property",
671 "punctuation.bracket",
672 "punctuation.delimiter",
673 "punctuation.special",
674 "string",
675 "string.special",
676 "type",
677 "type.builtin",
678 "variable",
679 "variable.builtin",
680 "variable.parameter",
681];
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686 use std::path::Path;
687
688 #[test]
689 fn test_lsp_language_id_tsx() {
690 let lang = Language::TypeScript;
691 assert_eq!(
692 lang.lsp_language_id(Path::new("app.tsx")),
693 "typescriptreact"
694 );
695 }
696
697 #[test]
698 fn test_lsp_language_id_ts() {
699 let lang = Language::TypeScript;
700 assert_eq!(lang.lsp_language_id(Path::new("app.ts")), "typescript");
701 }
702
703 #[test]
704 fn test_lsp_language_id_jsx() {
705 let lang = Language::JavaScript;
706 assert_eq!(
707 lang.lsp_language_id(Path::new("component.jsx")),
708 "javascriptreact"
709 );
710 }
711
712 #[test]
713 fn test_lsp_language_id_js() {
714 let lang = Language::JavaScript;
715 assert_eq!(lang.lsp_language_id(Path::new("app.js")), "javascript");
716 }
717
718 #[test]
719 fn test_lsp_language_id_csharp() {
720 let lang = Language::CSharp;
721 assert_eq!(lang.lsp_language_id(Path::new("main.cs")), "csharp");
722 }
723
724 #[test]
725 fn test_lsp_language_id_other_languages() {
726 assert_eq!(Language::Rust.lsp_language_id(Path::new("main.rs")), "rust");
727 assert_eq!(
728 Language::Python.lsp_language_id(Path::new("script.py")),
729 "python"
730 );
731 assert_eq!(Language::Go.lsp_language_id(Path::new("main.go")), "go");
732 }
733
734 #[test]
735 fn test_csharp_id_matches_config_key() {
736 assert_eq!(Language::CSharp.id(), "csharp");
739 }
740
741 #[test]
742 fn test_templ_detected_from_extension() {
743 let path = Path::new("home.templ");
744 assert!(matches!(Language::from_path(path), Some(Language::Templ)));
745 }
746
747 #[test]
748 #[cfg(feature = "tree-sitter-templ")]
749 fn test_templ_highlight_config_builds() {
750 Language::Templ
754 .highlight_config()
755 .expect("Templ highlight config should build");
756 }
757
758 #[test]
762 fn test_from_path_matches_extensions() {
763 for lang in Language::all() {
764 for ext in lang.extensions() {
765 let path = std::path::PathBuf::from(format!("x.{}", ext));
766 let detected = Language::from_path(&path).unwrap_or_else(|| {
767 panic!(
768 "extension .{} listed by {:?} but from_path returned None",
769 ext, lang
770 )
771 });
772 assert_eq!(
773 detected, *lang,
774 "extension .{} listed by {:?} but from_path returned {:?}",
775 ext, lang, detected
776 );
777 }
778 }
779 }
780}