Skip to main content

fresh_languages/
lib.rs

1use std::path::Path;
2
3// Re-export tree-sitter crates for use by fresh-editor
4pub use tree_sitter;
5pub use tree_sitter_highlight;
6pub use tree_sitter_highlight::HighlightConfiguration;
7
8// Re-export language crates (gated by features)
9#[cfg(feature = "tree-sitter-bash")]
10pub use tree_sitter_bash;
11#[cfg(feature = "tree-sitter-c")]
12pub use tree_sitter_c;
13#[cfg(feature = "tree-sitter-c-sharp")]
14pub use tree_sitter_c_sharp;
15#[cfg(feature = "tree-sitter-cpp")]
16pub use tree_sitter_cpp;
17#[cfg(feature = "tree-sitter-css")]
18pub use tree_sitter_css;
19#[cfg(feature = "tree-sitter-go")]
20pub use tree_sitter_go;
21#[cfg(feature = "tree-sitter-html")]
22pub use tree_sitter_html;
23#[cfg(feature = "tree-sitter-java")]
24pub use tree_sitter_java;
25#[cfg(feature = "tree-sitter-javascript")]
26pub use tree_sitter_javascript;
27#[cfg(feature = "tree-sitter-json")]
28pub use tree_sitter_json;
29#[cfg(feature = "tree-sitter-lua")]
30pub use tree_sitter_lua;
31#[cfg(feature = "tree-sitter-odin")]
32pub use tree_sitter_odin;
33#[cfg(feature = "tree-sitter-pascal")]
34pub use tree_sitter_pascal;
35#[cfg(feature = "tree-sitter-php")]
36pub use tree_sitter_php;
37#[cfg(feature = "tree-sitter-python")]
38pub use tree_sitter_python;
39#[cfg(feature = "tree-sitter-ruby")]
40pub use tree_sitter_ruby;
41#[cfg(feature = "tree-sitter-rust")]
42pub use tree_sitter_rust;
43#[cfg(feature = "tree-sitter-typescript")]
44pub use tree_sitter_typescript;
45
46/// Highlight category names used for default languages.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum HighlightCategory {
49    Attribute,
50    Comment,
51    Constant,
52    Function,
53    Keyword,
54    Number,
55    Operator,
56    PunctuationBracket,
57    PunctuationDelimiter,
58    Property,
59    String,
60    Type,
61    Variable,
62}
63
64impl HighlightCategory {
65    /// Map a default language highlight index to a category
66    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            _ => None,
82        }
83    }
84
85    /// Map a TypeScript highlight index to a category.
86    pub fn from_typescript_index(index: usize) -> Option<Self> {
87        match index {
88            0 => Some(Self::Attribute),             // attribute
89            1 => Some(Self::Comment),               // comment
90            2 => Some(Self::Constant),              // constant
91            3 => Some(Self::Constant),              // constant.builtin
92            4 => Some(Self::Type),                  // constructor
93            5 => Some(Self::String),                // embedded (template substitutions)
94            6 => Some(Self::Function),              // function
95            7 => Some(Self::Function),              // function.builtin
96            8 => Some(Self::Function),              // function.method
97            9 => Some(Self::Keyword),               // keyword
98            10 => Some(Self::Number),               // number
99            11 => Some(Self::Operator),             // operator
100            12 => Some(Self::Property),             // property
101            13 => Some(Self::PunctuationBracket),   // punctuation.bracket
102            14 => Some(Self::PunctuationDelimiter), // punctuation.delimiter
103            15 => Some(Self::Constant),             // punctuation.special (template ${})
104            16 => Some(Self::String),               // string
105            17 => Some(Self::String),               // string.special (regex)
106            18 => Some(Self::Type),                 // type
107            19 => Some(Self::Type),                 // type.builtin
108            20 => Some(Self::Variable),             // variable
109            21 => Some(Self::Constant),             // variable.builtin (this, super, arguments)
110            22 => Some(Self::Variable),             // variable.parameter
111            _ => None,
112        }
113    }
114
115    /// Get the theme key path for this category (e.g., "syntax.keyword").
116    pub fn theme_key(&self) -> &'static str {
117        match self {
118            Self::Keyword => "syntax.keyword",
119            Self::String => "syntax.string",
120            Self::Comment => "syntax.comment",
121            Self::Function => "syntax.function",
122            Self::Type => "syntax.type",
123            Self::Variable | Self::Property => "syntax.variable",
124            Self::Constant | Self::Number | Self::Attribute => "syntax.constant",
125            Self::Operator => "syntax.operator",
126            Self::PunctuationBracket => "syntax.punctuation_bracket",
127            Self::PunctuationDelimiter => "syntax.punctuation_delimiter",
128        }
129    }
130
131    /// Get a human-readable display name for this category.
132    pub fn display_name(&self) -> &'static str {
133        match self {
134            Self::Attribute => "Attribute",
135            Self::Comment => "Comment",
136            Self::Constant => "Constant",
137            Self::Function => "Function",
138            Self::Keyword => "Keyword",
139            Self::Number => "Number",
140            Self::Operator => "Operator",
141            Self::PunctuationBracket => "Punctuation Bracket",
142            Self::PunctuationDelimiter => "Punctuation Delimiter",
143            Self::Property => "Property",
144            Self::String => "String",
145            Self::Type => "Type",
146            Self::Variable => "Variable",
147        }
148    }
149}
150
151/// Language configuration for syntax highlighting
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum Language {
154    Rust,
155    Python,
156    JavaScript,
157    TypeScript,
158    HTML,
159    CSS,
160    C,
161    Cpp,
162    Go,
163    Json,
164    Java,
165    CSharp,
166    Php,
167    Ruby,
168    Bash,
169    Lua,
170    Pascal,
171    Odin,
172}
173
174impl Language {
175    /// Detect language from file extension
176    pub fn from_path(path: &Path) -> Option<Self> {
177        match path.extension()?.to_str()? {
178            "rs" => Some(Language::Rust),
179            "py" => Some(Language::Python),
180            "js" | "jsx" | "mjs" | "cjs" => Some(Language::JavaScript),
181            "ts" | "tsx" | "mts" | "cts" => Some(Language::TypeScript),
182            "html" => Some(Language::HTML),
183            "css" => Some(Language::CSS),
184            "c" | "h" => Some(Language::C),
185            "cpp" | "hpp" | "cc" | "hh" | "cxx" | "hxx" | "cppm" | "ixx" => Some(Language::Cpp),
186            "go" => Some(Language::Go),
187            "json" => Some(Language::Json),
188            "java" => Some(Language::Java),
189            "cs" => Some(Language::CSharp),
190            "php" => Some(Language::Php),
191            "rb" => Some(Language::Ruby),
192            "sh" | "bash" => Some(Language::Bash),
193            "lua" => Some(Language::Lua),
194            "pas" | "p" => Some(Language::Pascal),
195            "odin" => Some(Language::Odin),
196            _ => None,
197        }
198    }
199
200    /// Get tree-sitter highlight configuration for this language
201    pub fn highlight_config(&self) -> Result<HighlightConfiguration, String> {
202        match self {
203            Self::Rust => {
204                #[cfg(feature = "tree-sitter-rust")]
205                {
206                    let mut config = HighlightConfiguration::new(
207                        tree_sitter_rust::LANGUAGE.into(),
208                        "rust",
209                        tree_sitter_rust::HIGHLIGHTS_QUERY,
210                        "",
211                        "",
212                    )
213                    .map_err(|e| format!("Failed to create Rust highlight config: {e}"))?;
214                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
215                    Ok(config)
216                }
217                #[cfg(not(feature = "tree-sitter-rust"))]
218                Err("Rust language support not enabled".to_string())
219            }
220            Self::Python => {
221                #[cfg(feature = "tree-sitter-python")]
222                {
223                    let mut config = HighlightConfiguration::new(
224                        tree_sitter_python::LANGUAGE.into(),
225                        "python",
226                        tree_sitter_python::HIGHLIGHTS_QUERY,
227                        "",
228                        "",
229                    )
230                    .map_err(|e| format!("Failed to create Python highlight config: {e}"))?;
231                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
232                    Ok(config)
233                }
234                #[cfg(not(feature = "tree-sitter-python"))]
235                Err("Python language support not enabled".to_string())
236            }
237            Self::JavaScript => {
238                #[cfg(feature = "tree-sitter-javascript")]
239                {
240                    let mut config = HighlightConfiguration::new(
241                        tree_sitter_javascript::LANGUAGE.into(),
242                        "javascript",
243                        tree_sitter_javascript::HIGHLIGHT_QUERY,
244                        "",
245                        "",
246                    )
247                    .map_err(|e| format!("Failed to create JavaScript highlight config: {e}"))?;
248                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
249                    Ok(config)
250                }
251                #[cfg(not(feature = "tree-sitter-javascript"))]
252                Err("JavaScript language support not enabled".to_string())
253            }
254            Self::TypeScript => {
255                #[cfg(all(feature = "tree-sitter-typescript", feature = "tree-sitter-javascript"))]
256                {
257                    let combined_highlights = format!(
258                        "{}\n{}",
259                        tree_sitter_typescript::HIGHLIGHTS_QUERY,
260                        tree_sitter_javascript::HIGHLIGHT_QUERY
261                    );
262                    let mut config = HighlightConfiguration::new(
263                        tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
264                        "typescript",
265                        &combined_highlights,
266                        "",
267                        tree_sitter_typescript::LOCALS_QUERY,
268                    )
269                    .map_err(|e| format!("Failed to create TypeScript highlight config: {e}"))?;
270                    config.configure(TYPESCRIPT_HIGHLIGHT_CAPTURES);
271                    Ok(config)
272                }
273                #[cfg(not(all(
274                    feature = "tree-sitter-typescript",
275                    feature = "tree-sitter-javascript"
276                )))]
277                Err("TypeScript language support not enabled".to_string())
278            }
279            Self::HTML => {
280                #[cfg(feature = "tree-sitter-html")]
281                {
282                    let mut config = HighlightConfiguration::new(
283                        tree_sitter_html::LANGUAGE.into(),
284                        "html",
285                        tree_sitter_html::HIGHLIGHTS_QUERY,
286                        "",
287                        "",
288                    )
289                    .map_err(|e| format!("Failed to create HTML highlight config: {e}"))?;
290                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
291                    Ok(config)
292                }
293                #[cfg(not(feature = "tree-sitter-html"))]
294                Err("HTML language support not enabled".to_string())
295            }
296            Self::CSS => {
297                #[cfg(feature = "tree-sitter-css")]
298                {
299                    let mut config = HighlightConfiguration::new(
300                        tree_sitter_css::LANGUAGE.into(),
301                        "css",
302                        tree_sitter_css::HIGHLIGHTS_QUERY,
303                        "",
304                        "",
305                    )
306                    .map_err(|e| format!("Failed to create CSS highlight config: {e}"))?;
307                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
308                    Ok(config)
309                }
310                #[cfg(not(feature = "tree-sitter-css"))]
311                Err("CSS language support not enabled".to_string())
312            }
313            Self::C => {
314                #[cfg(feature = "tree-sitter-c")]
315                {
316                    let mut config = HighlightConfiguration::new(
317                        tree_sitter_c::LANGUAGE.into(),
318                        "c",
319                        tree_sitter_c::HIGHLIGHT_QUERY,
320                        "",
321                        "",
322                    )
323                    .map_err(|e| format!("Failed to create C highlight config: {e}"))?;
324                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
325                    Ok(config)
326                }
327                #[cfg(not(feature = "tree-sitter-c"))]
328                Err("C language support not enabled".to_string())
329            }
330            Self::Cpp => {
331                #[cfg(feature = "tree-sitter-cpp")]
332                {
333                    let mut config = HighlightConfiguration::new(
334                        tree_sitter_cpp::LANGUAGE.into(),
335                        "cpp",
336                        tree_sitter_cpp::HIGHLIGHT_QUERY,
337                        "",
338                        "",
339                    )
340                    .map_err(|e| format!("Failed to create C++ highlight config: {e}"))?;
341                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
342                    Ok(config)
343                }
344                #[cfg(not(feature = "tree-sitter-cpp"))]
345                Err("C++ language support not enabled".to_string())
346            }
347            Self::Go => {
348                #[cfg(feature = "tree-sitter-go")]
349                {
350                    let mut config = HighlightConfiguration::new(
351                        tree_sitter_go::LANGUAGE.into(),
352                        "go",
353                        tree_sitter_go::HIGHLIGHTS_QUERY,
354                        "",
355                        "",
356                    )
357                    .map_err(|e| format!("Failed to create Go highlight config: {e}"))?;
358                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
359                    Ok(config)
360                }
361                #[cfg(not(feature = "tree-sitter-go"))]
362                Err("Go language support not enabled".to_string())
363            }
364            Self::Json => {
365                #[cfg(feature = "tree-sitter-json")]
366                {
367                    let mut config = HighlightConfiguration::new(
368                        tree_sitter_json::LANGUAGE.into(),
369                        "json",
370                        tree_sitter_json::HIGHLIGHTS_QUERY,
371                        "",
372                        "",
373                    )
374                    .map_err(|e| format!("Failed to create JSON highlight config: {e}"))?;
375                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
376                    Ok(config)
377                }
378                #[cfg(not(feature = "tree-sitter-json"))]
379                Err("JSON language support not enabled".to_string())
380            }
381            Self::Java => {
382                #[cfg(feature = "tree-sitter-java")]
383                {
384                    let mut config = HighlightConfiguration::new(
385                        tree_sitter_java::LANGUAGE.into(),
386                        "java",
387                        tree_sitter_java::HIGHLIGHTS_QUERY,
388                        "",
389                        "",
390                    )
391                    .map_err(|e| format!("Failed to create Java highlight config: {e}"))?;
392                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
393                    Ok(config)
394                }
395                #[cfg(not(feature = "tree-sitter-java"))]
396                Err("Java language support not enabled".to_string())
397            }
398            Self::CSharp => {
399                #[cfg(feature = "tree-sitter-c-sharp")]
400                {
401                    let mut config = HighlightConfiguration::new(
402                        tree_sitter_c_sharp::LANGUAGE.into(),
403                        "c_sharp",
404                        "",
405                        "",
406                        "",
407                    )
408                    .map_err(|e| format!("Failed to create C# highlight config: {e}"))?;
409                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
410                    Ok(config)
411                }
412                #[cfg(not(feature = "tree-sitter-c-sharp"))]
413                Err("C# language support not enabled".to_string())
414            }
415            Self::Php => {
416                #[cfg(feature = "tree-sitter-php")]
417                {
418                    let mut config = HighlightConfiguration::new(
419                        tree_sitter_php::LANGUAGE_PHP.into(),
420                        "php",
421                        tree_sitter_php::HIGHLIGHTS_QUERY,
422                        "",
423                        "",
424                    )
425                    .map_err(|e| format!("Failed to create PHP highlight config: {e}"))?;
426                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
427                    Ok(config)
428                }
429                #[cfg(not(feature = "tree-sitter-php"))]
430                Err("PHP language support not enabled".to_string())
431            }
432            Self::Ruby => {
433                #[cfg(feature = "tree-sitter-ruby")]
434                {
435                    let mut config = HighlightConfiguration::new(
436                        tree_sitter_ruby::LANGUAGE.into(),
437                        "ruby",
438                        tree_sitter_ruby::HIGHLIGHTS_QUERY,
439                        "",
440                        "",
441                    )
442                    .map_err(|e| format!("Failed to create Ruby highlight config: {e}"))?;
443                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
444                    Ok(config)
445                }
446                #[cfg(not(feature = "tree-sitter-ruby"))]
447                Err("Ruby language support not enabled".to_string())
448            }
449            Self::Bash => {
450                #[cfg(feature = "tree-sitter-bash")]
451                {
452                    let mut config = HighlightConfiguration::new(
453                        tree_sitter_bash::LANGUAGE.into(),
454                        "bash",
455                        tree_sitter_bash::HIGHLIGHT_QUERY,
456                        "",
457                        "",
458                    )
459                    .map_err(|e| format!("Failed to create Bash highlight config: {e}"))?;
460                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
461                    Ok(config)
462                }
463                #[cfg(not(feature = "tree-sitter-bash"))]
464                Err("Bash language support not enabled".to_string())
465            }
466            Self::Lua => {
467                #[cfg(feature = "tree-sitter-lua")]
468                {
469                    let mut config = HighlightConfiguration::new(
470                        tree_sitter_lua::LANGUAGE.into(),
471                        "lua",
472                        tree_sitter_lua::HIGHLIGHTS_QUERY,
473                        "",
474                        "",
475                    )
476                    .map_err(|e| format!("Failed to create Lua highlight config: {e}"))?;
477                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
478                    Ok(config)
479                }
480                #[cfg(not(feature = "tree-sitter-lua"))]
481                Err("Lua language support not enabled".to_string())
482            }
483            Self::Pascal => {
484                #[cfg(feature = "tree-sitter-pascal")]
485                {
486                    let mut config = HighlightConfiguration::new(
487                        tree_sitter_pascal::LANGUAGE.into(),
488                        "pascal",
489                        "",
490                        "",
491                        "",
492                    )
493                    .map_err(|e| format!("Failed to create Pascal highlight config: {e}"))?;
494                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
495                    Ok(config)
496                }
497                #[cfg(not(feature = "tree-sitter-pascal"))]
498                Err("Pascal language support not enabled".to_string())
499            }
500            Self::Odin => {
501                #[cfg(feature = "tree-sitter-odin")]
502                {
503                    let mut config = HighlightConfiguration::new(
504                        tree_sitter_odin::LANGUAGE.into(),
505                        "odin",
506                        "",
507                        "",
508                        "",
509                    )
510                    .map_err(|e| format!("Failed to create Odin highlight config: {e}"))?;
511                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
512                    Ok(config)
513                }
514                #[cfg(not(feature = "tree-sitter-odin"))]
515                Err("Odin language support not enabled".to_string())
516            }
517        }
518    }
519
520    /// Map tree-sitter highlight index to a highlight category
521    pub fn highlight_category(&self, index: usize) -> Option<HighlightCategory> {
522        match self {
523            Self::TypeScript => HighlightCategory::from_typescript_index(index),
524            _ => HighlightCategory::from_default_index(index),
525        }
526    }
527}
528
529impl Language {
530    /// Returns all available language variants
531    pub fn all() -> &'static [Language] {
532        &[
533            Language::Rust,
534            Language::Python,
535            Language::JavaScript,
536            Language::TypeScript,
537            Language::HTML,
538            Language::CSS,
539            Language::C,
540            Language::Cpp,
541            Language::Go,
542            Language::Json,
543            Language::Java,
544            Language::CSharp,
545            Language::Php,
546            Language::Ruby,
547            Language::Bash,
548            Language::Lua,
549            Language::Pascal,
550            Language::Odin,
551        ]
552    }
553
554    /// Returns the language ID (lowercase identifier used in config/internal)
555    pub fn id(&self) -> &'static str {
556        match self {
557            Self::Rust => "rust",
558            Self::Python => "python",
559            Self::JavaScript => "javascript",
560            Self::TypeScript => "typescript",
561            Self::HTML => "html",
562            Self::CSS => "css",
563            Self::C => "c",
564            Self::Cpp => "cpp",
565            Self::Go => "go",
566            Self::Json => "json",
567            Self::Java => "java",
568            Self::CSharp => "csharp",
569            Self::Php => "php",
570            Self::Ruby => "ruby",
571            Self::Bash => "bash",
572            Self::Lua => "lua",
573            Self::Pascal => "pascal",
574            Self::Odin => "odin",
575        }
576    }
577
578    /// Returns the LSP languageId for use in textDocument/didOpen.
579    ///
580    /// This considers the file extension to return the correct LSP-spec language ID.
581    /// For example, `.tsx` files return `"typescriptreact"` instead of `"typescript"`,
582    /// and `.jsx` files return `"javascriptreact"` instead of `"javascript"`.
583    ///
584    /// See: <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem>
585    pub fn lsp_language_id(&self, path: &Path) -> &'static str {
586        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
587        match (self, ext) {
588            (Self::TypeScript, "tsx") => "typescriptreact",
589            (Self::JavaScript, "jsx") => "javascriptreact",
590            _ => self.id(),
591        }
592    }
593
594    /// Returns the human-readable display name
595    pub fn display_name(&self) -> &'static str {
596        match self {
597            Self::Rust => "Rust",
598            Self::Python => "Python",
599            Self::JavaScript => "JavaScript",
600            Self::TypeScript => "TypeScript",
601            Self::HTML => "HTML",
602            Self::CSS => "CSS",
603            Self::C => "C",
604            Self::Cpp => "C++",
605            Self::Go => "Go",
606            Self::Json => "JSON",
607            Self::Java => "Java",
608            Self::CSharp => "C#",
609            Self::Php => "PHP",
610            Self::Ruby => "Ruby",
611            Self::Bash => "Bash",
612            Self::Lua => "Lua",
613            Self::Pascal => "Pascal",
614            Self::Odin => "Odin",
615        }
616    }
617
618    /// Parse a language from its ID or display name
619    pub fn from_id(id: &str) -> Option<Self> {
620        let id_lower = id.to_lowercase();
621        match id_lower.as_str() {
622            "rust" => Some(Self::Rust),
623            "python" => Some(Self::Python),
624            "javascript" => Some(Self::JavaScript),
625            "typescript" => Some(Self::TypeScript),
626            "html" => Some(Self::HTML),
627            "css" => Some(Self::CSS),
628            "c" => Some(Self::C),
629            "cpp" | "c++" => Some(Self::Cpp),
630            "go" => Some(Self::Go),
631            "json" => Some(Self::Json),
632            "java" => Some(Self::Java),
633            "c_sharp" | "c#" | "csharp" => Some(Self::CSharp),
634            "php" => Some(Self::Php),
635            "ruby" => Some(Self::Ruby),
636            "bash" => Some(Self::Bash),
637            "lua" => Some(Self::Lua),
638            "pascal" => Some(Self::Pascal),
639            "odin" => Some(Self::Odin),
640            _ => None,
641        }
642    }
643
644    /// Try to map a syntect syntax name to a tree-sitter Language.
645    ///
646    /// This is used to get tree-sitter features (indentation, semantic highlighting)
647    /// when using a syntect grammar for syntax highlighting. This is best-effort since
648    /// tree-sitter only supports ~18 languages while syntect supports 100+.
649    ///
650    /// Syntect uses names like "Rust", "Python", "JavaScript", "JSON", "C++", "C#",
651    /// "Bourne Again Shell (bash)", etc.
652    pub fn from_name(name: &str) -> Option<Self> {
653        // First try exact display name match
654        for lang in Self::all() {
655            if lang.display_name() == name {
656                return Some(*lang);
657            }
658        }
659
660        // Then try case-insensitive matching and common aliases
661        let name_lower = name.to_lowercase();
662        match name_lower.as_str() {
663            "rust" => Some(Self::Rust),
664            "python" => Some(Self::Python),
665            "javascript" | "javascript (babel)" => Some(Self::JavaScript),
666            "typescript" | "typescriptreact" => Some(Self::TypeScript),
667            "html" => Some(Self::HTML),
668            "css" => Some(Self::CSS),
669            "c" => Some(Self::C),
670            "c++" => Some(Self::Cpp),
671            "go" | "golang" => Some(Self::Go),
672            "json" => Some(Self::Json),
673            "java" => Some(Self::Java),
674            "c#" => Some(Self::CSharp),
675            "php" => Some(Self::Php),
676            "ruby" => Some(Self::Ruby),
677            "lua" => Some(Self::Lua),
678            "pascal" => Some(Self::Pascal),
679            "odin" => Some(Self::Odin),
680            _ => {
681                // Try matching shell variants
682                if name_lower.contains("bash") || name_lower.contains("shell") {
683                    return Some(Self::Bash);
684                }
685                None
686            }
687        }
688    }
689}
690
691impl std::fmt::Display for Language {
692    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
693        write!(f, "{}", self.id())
694    }
695}
696
697const DEFAULT_HIGHLIGHT_CAPTURES: &[&str] = &[
698    "attribute",
699    "comment",
700    "constant",
701    "function",
702    "keyword",
703    "number",
704    "operator",
705    "punctuation.bracket",
706    "punctuation.delimiter",
707    "property",
708    "string",
709    "type",
710    "variable",
711];
712
713const TYPESCRIPT_HIGHLIGHT_CAPTURES: &[&str] = &[
714    "attribute",
715    "comment",
716    "constant",
717    "constant.builtin",
718    "constructor",
719    "embedded",
720    "function",
721    "function.builtin",
722    "function.method",
723    "keyword",
724    "number",
725    "operator",
726    "property",
727    "punctuation.bracket",
728    "punctuation.delimiter",
729    "punctuation.special",
730    "string",
731    "string.special",
732    "type",
733    "type.builtin",
734    "variable",
735    "variable.builtin",
736    "variable.parameter",
737];
738
739#[cfg(test)]
740mod tests {
741    use super::*;
742    use std::path::Path;
743
744    #[test]
745    fn test_lsp_language_id_tsx() {
746        let lang = Language::TypeScript;
747        assert_eq!(
748            lang.lsp_language_id(Path::new("app.tsx")),
749            "typescriptreact"
750        );
751    }
752
753    #[test]
754    fn test_lsp_language_id_ts() {
755        let lang = Language::TypeScript;
756        assert_eq!(lang.lsp_language_id(Path::new("app.ts")), "typescript");
757    }
758
759    #[test]
760    fn test_lsp_language_id_jsx() {
761        let lang = Language::JavaScript;
762        assert_eq!(
763            lang.lsp_language_id(Path::new("component.jsx")),
764            "javascriptreact"
765        );
766    }
767
768    #[test]
769    fn test_lsp_language_id_js() {
770        let lang = Language::JavaScript;
771        assert_eq!(lang.lsp_language_id(Path::new("app.js")), "javascript");
772    }
773
774    #[test]
775    fn test_lsp_language_id_csharp() {
776        let lang = Language::CSharp;
777        assert_eq!(lang.lsp_language_id(Path::new("main.cs")), "csharp");
778    }
779
780    #[test]
781    fn test_lsp_language_id_other_languages() {
782        assert_eq!(Language::Rust.lsp_language_id(Path::new("main.rs")), "rust");
783        assert_eq!(
784            Language::Python.lsp_language_id(Path::new("script.py")),
785            "python"
786        );
787        assert_eq!(Language::Go.lsp_language_id(Path::new("main.go")), "go");
788    }
789
790    #[test]
791    fn test_csharp_id_matches_config_key() {
792        // Language::id() must return "csharp" to match the config key
793        // used for LSP server lookup and language detection.
794        assert_eq!(Language::CSharp.id(), "csharp");
795    }
796}