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