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