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