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