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, Hash)]
153pub enum Language {
154    Rust,
155    Python,
156    JavaScript,
157    TypeScript,
158    HTML,
159    CSS,
160    C,
161    Cpp,
162    Go,
163    Json,
164    Jsonc,
165    Java,
166    CSharp,
167    Php,
168    Ruby,
169    Bash,
170    Lua,
171    Pascal,
172    Odin,
173}
174
175impl Language {
176    /// Detect language from file extension.
177    ///
178    /// Derived from `extensions()` — see `Self::all` / `Self::extensions` for
179    /// the authoritative table. A linear scan over ~18 languages is cheap
180    /// enough that the nicer invariant (no duplicate tables) beats a match.
181    pub fn from_path(path: &Path) -> Option<Self> {
182        let ext = path.extension()?.to_str()?;
183        Self::all()
184            .iter()
185            .find(|lang| lang.extensions().contains(&ext))
186            .copied()
187    }
188
189    /// Get tree-sitter highlight configuration for this language
190    pub fn highlight_config(&self) -> Result<HighlightConfiguration, String> {
191        match self {
192            Self::Rust => {
193                #[cfg(feature = "tree-sitter-rust")]
194                {
195                    let mut config = HighlightConfiguration::new(
196                        tree_sitter_rust::LANGUAGE.into(),
197                        "rust",
198                        tree_sitter_rust::HIGHLIGHTS_QUERY,
199                        "",
200                        "",
201                    )
202                    .map_err(|e| format!("Failed to create Rust highlight config: {e}"))?;
203                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
204                    Ok(config)
205                }
206                #[cfg(not(feature = "tree-sitter-rust"))]
207                Err("Rust language support not enabled".to_string())
208            }
209            Self::Python => {
210                #[cfg(feature = "tree-sitter-python")]
211                {
212                    let mut config = HighlightConfiguration::new(
213                        tree_sitter_python::LANGUAGE.into(),
214                        "python",
215                        tree_sitter_python::HIGHLIGHTS_QUERY,
216                        "",
217                        "",
218                    )
219                    .map_err(|e| format!("Failed to create Python highlight config: {e}"))?;
220                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
221                    Ok(config)
222                }
223                #[cfg(not(feature = "tree-sitter-python"))]
224                Err("Python language support not enabled".to_string())
225            }
226            Self::JavaScript => {
227                #[cfg(feature = "tree-sitter-javascript")]
228                {
229                    let mut config = HighlightConfiguration::new(
230                        tree_sitter_javascript::LANGUAGE.into(),
231                        "javascript",
232                        tree_sitter_javascript::HIGHLIGHT_QUERY,
233                        "",
234                        "",
235                    )
236                    .map_err(|e| format!("Failed to create JavaScript highlight config: {e}"))?;
237                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
238                    Ok(config)
239                }
240                #[cfg(not(feature = "tree-sitter-javascript"))]
241                Err("JavaScript language support not enabled".to_string())
242            }
243            Self::TypeScript => {
244                #[cfg(all(feature = "tree-sitter-typescript", feature = "tree-sitter-javascript"))]
245                {
246                    let combined_highlights = format!(
247                        "{}\n{}",
248                        tree_sitter_typescript::HIGHLIGHTS_QUERY,
249                        tree_sitter_javascript::HIGHLIGHT_QUERY
250                    );
251                    let mut config = HighlightConfiguration::new(
252                        tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
253                        "typescript",
254                        &combined_highlights,
255                        "",
256                        tree_sitter_typescript::LOCALS_QUERY,
257                    )
258                    .map_err(|e| format!("Failed to create TypeScript highlight config: {e}"))?;
259                    config.configure(TYPESCRIPT_HIGHLIGHT_CAPTURES);
260                    Ok(config)
261                }
262                #[cfg(not(all(
263                    feature = "tree-sitter-typescript",
264                    feature = "tree-sitter-javascript"
265                )))]
266                Err("TypeScript language support not enabled".to_string())
267            }
268            Self::HTML => {
269                #[cfg(feature = "tree-sitter-html")]
270                {
271                    let mut config = HighlightConfiguration::new(
272                        tree_sitter_html::LANGUAGE.into(),
273                        "html",
274                        tree_sitter_html::HIGHLIGHTS_QUERY,
275                        "",
276                        "",
277                    )
278                    .map_err(|e| format!("Failed to create HTML highlight config: {e}"))?;
279                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
280                    Ok(config)
281                }
282                #[cfg(not(feature = "tree-sitter-html"))]
283                Err("HTML language support not enabled".to_string())
284            }
285            Self::CSS => {
286                #[cfg(feature = "tree-sitter-css")]
287                {
288                    let mut config = HighlightConfiguration::new(
289                        tree_sitter_css::LANGUAGE.into(),
290                        "css",
291                        tree_sitter_css::HIGHLIGHTS_QUERY,
292                        "",
293                        "",
294                    )
295                    .map_err(|e| format!("Failed to create CSS highlight config: {e}"))?;
296                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
297                    Ok(config)
298                }
299                #[cfg(not(feature = "tree-sitter-css"))]
300                Err("CSS language support not enabled".to_string())
301            }
302            Self::C => {
303                #[cfg(feature = "tree-sitter-c")]
304                {
305                    let mut config = HighlightConfiguration::new(
306                        tree_sitter_c::LANGUAGE.into(),
307                        "c",
308                        tree_sitter_c::HIGHLIGHT_QUERY,
309                        "",
310                        "",
311                    )
312                    .map_err(|e| format!("Failed to create C highlight config: {e}"))?;
313                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
314                    Ok(config)
315                }
316                #[cfg(not(feature = "tree-sitter-c"))]
317                Err("C language support not enabled".to_string())
318            }
319            Self::Cpp => {
320                #[cfg(feature = "tree-sitter-cpp")]
321                {
322                    let mut config = HighlightConfiguration::new(
323                        tree_sitter_cpp::LANGUAGE.into(),
324                        "cpp",
325                        tree_sitter_cpp::HIGHLIGHT_QUERY,
326                        "",
327                        "",
328                    )
329                    .map_err(|e| format!("Failed to create C++ highlight config: {e}"))?;
330                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
331                    Ok(config)
332                }
333                #[cfg(not(feature = "tree-sitter-cpp"))]
334                Err("C++ language support not enabled".to_string())
335            }
336            Self::Go => {
337                #[cfg(feature = "tree-sitter-go")]
338                {
339                    let mut config = HighlightConfiguration::new(
340                        tree_sitter_go::LANGUAGE.into(),
341                        "go",
342                        tree_sitter_go::HIGHLIGHTS_QUERY,
343                        "",
344                        "",
345                    )
346                    .map_err(|e| format!("Failed to create Go highlight config: {e}"))?;
347                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
348                    Ok(config)
349                }
350                #[cfg(not(feature = "tree-sitter-go"))]
351                Err("Go language support not enabled".to_string())
352            }
353            Self::Json => {
354                #[cfg(feature = "tree-sitter-json")]
355                {
356                    let mut config = HighlightConfiguration::new(
357                        tree_sitter_json::LANGUAGE.into(),
358                        "json",
359                        tree_sitter_json::HIGHLIGHTS_QUERY,
360                        "",
361                        "",
362                    )
363                    .map_err(|e| format!("Failed to create JSON highlight config: {e}"))?;
364                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
365                    Ok(config)
366                }
367                #[cfg(not(feature = "tree-sitter-json"))]
368                Err("JSON language support not enabled".to_string())
369            }
370            Self::Jsonc => {
371                // JSONC (JSON with Comments) reuses the tree-sitter-json parser.
372                // A dedicated JSONC grammar isn't published as a Rust crate; the
373                // JSON parser recovers past comments and trailing commas well
374                // enough for highlighting, which is the only consumer here.
375                #[cfg(feature = "tree-sitter-json")]
376                {
377                    let mut config = HighlightConfiguration::new(
378                        tree_sitter_json::LANGUAGE.into(),
379                        "jsonc",
380                        tree_sitter_json::HIGHLIGHTS_QUERY,
381                        "",
382                        "",
383                    )
384                    .map_err(|e| format!("Failed to create JSONC highlight config: {e}"))?;
385                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
386                    Ok(config)
387                }
388                #[cfg(not(feature = "tree-sitter-json"))]
389                Err("JSONC language support not enabled".to_string())
390            }
391            Self::Java => {
392                #[cfg(feature = "tree-sitter-java")]
393                {
394                    let mut config = HighlightConfiguration::new(
395                        tree_sitter_java::LANGUAGE.into(),
396                        "java",
397                        tree_sitter_java::HIGHLIGHTS_QUERY,
398                        "",
399                        "",
400                    )
401                    .map_err(|e| format!("Failed to create Java highlight config: {e}"))?;
402                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
403                    Ok(config)
404                }
405                #[cfg(not(feature = "tree-sitter-java"))]
406                Err("Java language support not enabled".to_string())
407            }
408            Self::CSharp => {
409                #[cfg(feature = "tree-sitter-c-sharp")]
410                {
411                    let mut config = HighlightConfiguration::new(
412                        tree_sitter_c_sharp::LANGUAGE.into(),
413                        "c_sharp",
414                        "",
415                        "",
416                        "",
417                    )
418                    .map_err(|e| format!("Failed to create C# highlight config: {e}"))?;
419                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
420                    Ok(config)
421                }
422                #[cfg(not(feature = "tree-sitter-c-sharp"))]
423                Err("C# language support not enabled".to_string())
424            }
425            Self::Php => {
426                #[cfg(feature = "tree-sitter-php")]
427                {
428                    let mut config = HighlightConfiguration::new(
429                        tree_sitter_php::LANGUAGE_PHP.into(),
430                        "php",
431                        tree_sitter_php::HIGHLIGHTS_QUERY,
432                        "",
433                        "",
434                    )
435                    .map_err(|e| format!("Failed to create PHP highlight config: {e}"))?;
436                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
437                    Ok(config)
438                }
439                #[cfg(not(feature = "tree-sitter-php"))]
440                Err("PHP language support not enabled".to_string())
441            }
442            Self::Ruby => {
443                #[cfg(feature = "tree-sitter-ruby")]
444                {
445                    let mut config = HighlightConfiguration::new(
446                        tree_sitter_ruby::LANGUAGE.into(),
447                        "ruby",
448                        tree_sitter_ruby::HIGHLIGHTS_QUERY,
449                        "",
450                        "",
451                    )
452                    .map_err(|e| format!("Failed to create Ruby highlight config: {e}"))?;
453                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
454                    Ok(config)
455                }
456                #[cfg(not(feature = "tree-sitter-ruby"))]
457                Err("Ruby language support not enabled".to_string())
458            }
459            Self::Bash => {
460                #[cfg(feature = "tree-sitter-bash")]
461                {
462                    let mut config = HighlightConfiguration::new(
463                        tree_sitter_bash::LANGUAGE.into(),
464                        "bash",
465                        tree_sitter_bash::HIGHLIGHT_QUERY,
466                        "",
467                        "",
468                    )
469                    .map_err(|e| format!("Failed to create Bash highlight config: {e}"))?;
470                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
471                    Ok(config)
472                }
473                #[cfg(not(feature = "tree-sitter-bash"))]
474                Err("Bash language support not enabled".to_string())
475            }
476            Self::Lua => {
477                #[cfg(feature = "tree-sitter-lua")]
478                {
479                    let mut config = HighlightConfiguration::new(
480                        tree_sitter_lua::LANGUAGE.into(),
481                        "lua",
482                        tree_sitter_lua::HIGHLIGHTS_QUERY,
483                        "",
484                        "",
485                    )
486                    .map_err(|e| format!("Failed to create Lua highlight config: {e}"))?;
487                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
488                    Ok(config)
489                }
490                #[cfg(not(feature = "tree-sitter-lua"))]
491                Err("Lua language support not enabled".to_string())
492            }
493            Self::Pascal => {
494                #[cfg(feature = "tree-sitter-pascal")]
495                {
496                    let mut config = HighlightConfiguration::new(
497                        tree_sitter_pascal::LANGUAGE.into(),
498                        "pascal",
499                        "",
500                        "",
501                        "",
502                    )
503                    .map_err(|e| format!("Failed to create Pascal highlight config: {e}"))?;
504                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
505                    Ok(config)
506                }
507                #[cfg(not(feature = "tree-sitter-pascal"))]
508                Err("Pascal language support not enabled".to_string())
509            }
510            Self::Odin => {
511                #[cfg(feature = "tree-sitter-odin")]
512                {
513                    let mut config = HighlightConfiguration::new(
514                        tree_sitter_odin::LANGUAGE.into(),
515                        "odin",
516                        "",
517                        "",
518                        "",
519                    )
520                    .map_err(|e| format!("Failed to create Odin highlight config: {e}"))?;
521                    config.configure(DEFAULT_HIGHLIGHT_CAPTURES);
522                    Ok(config)
523                }
524                #[cfg(not(feature = "tree-sitter-odin"))]
525                Err("Odin language support not enabled".to_string())
526            }
527        }
528    }
529
530    /// Map tree-sitter highlight index to a highlight category
531    pub fn highlight_category(&self, index: usize) -> Option<HighlightCategory> {
532        match self {
533            Self::TypeScript => HighlightCategory::from_typescript_index(index),
534            _ => HighlightCategory::from_default_index(index),
535        }
536    }
537}
538
539impl Language {
540    /// Returns all available language variants
541    pub fn all() -> &'static [Language] {
542        &[
543            Language::Rust,
544            Language::Python,
545            Language::JavaScript,
546            Language::TypeScript,
547            Language::HTML,
548            Language::CSS,
549            Language::C,
550            Language::Cpp,
551            Language::Go,
552            Language::Json,
553            Language::Jsonc,
554            Language::Java,
555            Language::CSharp,
556            Language::Php,
557            Language::Ruby,
558            Language::Bash,
559            Language::Lua,
560            Language::Pascal,
561            Language::Odin,
562        ]
563    }
564
565    /// Returns the language ID (lowercase identifier used in config/internal)
566    pub fn id(&self) -> &'static str {
567        match self {
568            Self::Rust => "rust",
569            Self::Python => "python",
570            Self::JavaScript => "javascript",
571            Self::TypeScript => "typescript",
572            Self::HTML => "html",
573            Self::CSS => "css",
574            Self::C => "c",
575            Self::Cpp => "cpp",
576            Self::Go => "go",
577            Self::Json => "json",
578            Self::Jsonc => "jsonc",
579            Self::Java => "java",
580            Self::CSharp => "csharp",
581            Self::Php => "php",
582            Self::Ruby => "ruby",
583            Self::Bash => "bash",
584            Self::Lua => "lua",
585            Self::Pascal => "pascal",
586            Self::Odin => "odin",
587        }
588    }
589
590    /// Returns the LSP languageId for use in textDocument/didOpen.
591    ///
592    /// This considers the file extension to return the correct LSP-spec language ID.
593    /// For example, `.tsx` files return `"typescriptreact"` instead of `"typescript"`,
594    /// and `.jsx` files return `"javascriptreact"` instead of `"javascript"`.
595    ///
596    /// See: <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem>
597    pub fn lsp_language_id(&self, path: &Path) -> &'static str {
598        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
599        match (self, ext) {
600            (Self::TypeScript, "tsx") => "typescriptreact",
601            (Self::JavaScript, "jsx") => "javascriptreact",
602            _ => self.id(),
603        }
604    }
605
606    /// File extensions associated with this language.
607    ///
608    /// Keep in sync with `from_path`. Used by the grammar catalog so that
609    /// tree-sitter-only languages (like TypeScript) still advertise the
610    /// extensions they can highlight.
611    pub fn extensions(&self) -> &'static [&'static str] {
612        match self {
613            Self::Rust => &["rs"],
614            Self::Python => &["py"],
615            Self::JavaScript => &["js", "jsx", "mjs", "cjs"],
616            Self::TypeScript => &["ts", "tsx", "mts", "cts"],
617            Self::HTML => &["html"],
618            Self::CSS => &["css"],
619            Self::C => &["c", "h"],
620            Self::Cpp => &["cpp", "hpp", "cc", "hh", "cxx", "hxx", "cppm", "ixx"],
621            Self::Go => &["go"],
622            Self::Json => &["json"],
623            Self::Jsonc => &["jsonc"],
624            Self::Java => &["java"],
625            Self::CSharp => &["cs"],
626            Self::Php => &["php"],
627            Self::Ruby => &["rb"],
628            Self::Bash => &["sh", "bash"],
629            Self::Lua => &["lua"],
630            Self::Pascal => &["pas", "p"],
631            Self::Odin => &["odin"],
632        }
633    }
634
635    /// Returns the human-readable display name
636    pub fn display_name(&self) -> &'static str {
637        match self {
638            Self::Rust => "Rust",
639            Self::Python => "Python",
640            Self::JavaScript => "JavaScript",
641            Self::TypeScript => "TypeScript",
642            Self::HTML => "HTML",
643            Self::CSS => "CSS",
644            Self::C => "C",
645            Self::Cpp => "C++",
646            Self::Go => "Go",
647            Self::Json => "JSON",
648            Self::Jsonc => "JSON with Comments",
649            Self::Java => "Java",
650            Self::CSharp => "C#",
651            Self::Php => "PHP",
652            Self::Ruby => "Ruby",
653            Self::Bash => "Bash",
654            Self::Lua => "Lua",
655            Self::Pascal => "Pascal",
656            Self::Odin => "Odin",
657        }
658    }
659
660    /// Parse a language from its ID or display name
661    pub fn from_id(id: &str) -> Option<Self> {
662        let id_lower = id.to_lowercase();
663        match id_lower.as_str() {
664            "rust" => Some(Self::Rust),
665            "python" => Some(Self::Python),
666            "javascript" => Some(Self::JavaScript),
667            "typescript" => Some(Self::TypeScript),
668            "html" => Some(Self::HTML),
669            "css" => Some(Self::CSS),
670            "c" => Some(Self::C),
671            "cpp" | "c++" => Some(Self::Cpp),
672            "go" => Some(Self::Go),
673            "json" => Some(Self::Json),
674            "jsonc" => Some(Self::Jsonc),
675            "java" => Some(Self::Java),
676            "c_sharp" | "c#" | "csharp" => Some(Self::CSharp),
677            "php" => Some(Self::Php),
678            "ruby" => Some(Self::Ruby),
679            "bash" => Some(Self::Bash),
680            "lua" => Some(Self::Lua),
681            "pascal" => Some(Self::Pascal),
682            "odin" => Some(Self::Odin),
683            _ => None,
684        }
685    }
686
687    /// Try to map a syntect syntax name to a tree-sitter Language.
688    ///
689    /// This is used to get tree-sitter features (indentation, semantic highlighting)
690    /// when using a syntect grammar for syntax highlighting. This is best-effort since
691    /// tree-sitter only supports ~18 languages while syntect supports 100+.
692    ///
693    /// Syntect uses names like "Rust", "Python", "JavaScript", "JSON", "C++", "C#",
694    /// "Bourne Again Shell (bash)", etc.
695    pub fn from_name(name: &str) -> Option<Self> {
696        // First try exact display name match
697        for lang in Self::all() {
698            if lang.display_name() == name {
699                return Some(*lang);
700            }
701        }
702
703        // Then try case-insensitive matching and common aliases
704        let name_lower = name.to_lowercase();
705        match name_lower.as_str() {
706            "rust" => Some(Self::Rust),
707            "python" => Some(Self::Python),
708            "javascript" | "javascript (babel)" => Some(Self::JavaScript),
709            "typescript" | "typescriptreact" => Some(Self::TypeScript),
710            "html" => Some(Self::HTML),
711            "css" => Some(Self::CSS),
712            "c" => Some(Self::C),
713            "c++" => Some(Self::Cpp),
714            "go" | "golang" => Some(Self::Go),
715            "json" => Some(Self::Json),
716            "jsonc" | "json with comments" => Some(Self::Jsonc),
717            "java" => Some(Self::Java),
718            "c#" => Some(Self::CSharp),
719            "php" => Some(Self::Php),
720            "ruby" => Some(Self::Ruby),
721            "lua" => Some(Self::Lua),
722            "pascal" => Some(Self::Pascal),
723            "odin" => Some(Self::Odin),
724            _ => {
725                // Try matching shell variants
726                if name_lower.contains("bash") || name_lower.contains("shell") {
727                    return Some(Self::Bash);
728                }
729                None
730            }
731        }
732    }
733}
734
735impl std::fmt::Display for Language {
736    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
737        write!(f, "{}", self.id())
738    }
739}
740
741const DEFAULT_HIGHLIGHT_CAPTURES: &[&str] = &[
742    "attribute",
743    "comment",
744    "constant",
745    "function",
746    "keyword",
747    "number",
748    "operator",
749    "punctuation.bracket",
750    "punctuation.delimiter",
751    "property",
752    "string",
753    "type",
754    "variable",
755];
756
757const TYPESCRIPT_HIGHLIGHT_CAPTURES: &[&str] = &[
758    "attribute",
759    "comment",
760    "constant",
761    "constant.builtin",
762    "constructor",
763    "embedded",
764    "function",
765    "function.builtin",
766    "function.method",
767    "keyword",
768    "number",
769    "operator",
770    "property",
771    "punctuation.bracket",
772    "punctuation.delimiter",
773    "punctuation.special",
774    "string",
775    "string.special",
776    "type",
777    "type.builtin",
778    "variable",
779    "variable.builtin",
780    "variable.parameter",
781];
782
783#[cfg(test)]
784mod tests {
785    use super::*;
786    use std::path::Path;
787
788    #[test]
789    fn test_lsp_language_id_tsx() {
790        let lang = Language::TypeScript;
791        assert_eq!(
792            lang.lsp_language_id(Path::new("app.tsx")),
793            "typescriptreact"
794        );
795    }
796
797    #[test]
798    fn test_lsp_language_id_ts() {
799        let lang = Language::TypeScript;
800        assert_eq!(lang.lsp_language_id(Path::new("app.ts")), "typescript");
801    }
802
803    #[test]
804    fn test_lsp_language_id_jsx() {
805        let lang = Language::JavaScript;
806        assert_eq!(
807            lang.lsp_language_id(Path::new("component.jsx")),
808            "javascriptreact"
809        );
810    }
811
812    #[test]
813    fn test_lsp_language_id_js() {
814        let lang = Language::JavaScript;
815        assert_eq!(lang.lsp_language_id(Path::new("app.js")), "javascript");
816    }
817
818    #[test]
819    fn test_lsp_language_id_csharp() {
820        let lang = Language::CSharp;
821        assert_eq!(lang.lsp_language_id(Path::new("main.cs")), "csharp");
822    }
823
824    #[test]
825    fn test_lsp_language_id_other_languages() {
826        assert_eq!(Language::Rust.lsp_language_id(Path::new("main.rs")), "rust");
827        assert_eq!(
828            Language::Python.lsp_language_id(Path::new("script.py")),
829            "python"
830        );
831        assert_eq!(Language::Go.lsp_language_id(Path::new("main.go")), "go");
832    }
833
834    #[test]
835    fn test_csharp_id_matches_config_key() {
836        // Language::id() must return "csharp" to match the config key
837        // used for LSP server lookup and language detection.
838        assert_eq!(Language::CSharp.id(), "csharp");
839    }
840
841    /// Guard: `from_path` and `extensions()` must stay in sync — they used to
842    /// be two hand-maintained tables with a "keep in sync" comment, which
843    /// silently drifted when either was edited in isolation.
844    #[test]
845    fn test_from_path_matches_extensions() {
846        for lang in Language::all() {
847            for ext in lang.extensions() {
848                let path = std::path::PathBuf::from(format!("x.{}", ext));
849                let detected = Language::from_path(&path).unwrap_or_else(|| {
850                    panic!(
851                        "extension .{} listed by {:?} but from_path returned None",
852                        ext, lang
853                    )
854                });
855                assert_eq!(
856                    detected, *lang,
857                    "extension .{} listed by {:?} but from_path returned {:?}",
858                    ext, lang, detected
859                );
860            }
861        }
862    }
863}