Skip to main content

strop_syntax/
languages.rs

1//! One statically linked registry for native filenames, shebangs and injections.
2//! Queries are vendored from Helix under MPL-2.0; no runtime grammar downloads.
3use std::path::Path;
4use tree_sitter::Language;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum LanguageId {
8    Rust,
9    Python,
10    JavaScript,
11    TypeScript,
12    Tsx,
13    Go,
14    C,
15    Cpp,
16    Json,
17    Bash,
18    Fish,
19    Lua,
20    Sql,
21    CMake,
22    Markdown,
23    MarkdownInline,
24    Java,
25    CSharp,
26    Ruby,
27    Php,
28    Toml,
29    Yaml,
30    Html,
31    Css,
32}
33
34pub struct LanguageSpec {
35    pub id: LanguageId,
36    pub name: &'static str,
37    pub language: Language,
38    pub highlights: &'static str,
39    pub injections: &'static str,
40}
41struct Entry {
42    id: LanguageId,
43    name: &'static str,
44    grammar: fn() -> Language,
45    extensions: &'static [&'static str],
46    filenames: &'static [&'static str],
47    interpreters: &'static [&'static str],
48    aliases: &'static [&'static str],
49    highlights: &'static str,
50    injections: &'static str,
51}
52impl Entry {
53    fn spec(&self) -> LanguageSpec {
54        LanguageSpec {
55            id: self.id,
56            name: self.name,
57            language: (self.grammar)(),
58            highlights: self.highlights,
59            injections: self.injections,
60        }
61    }
62}
63
64static LANGUAGES: &[Entry] = &[
65    Entry {
66        id: LanguageId::Rust,
67        name: "rust",
68        grammar: || tree_sitter_rust::LANGUAGE.into(),
69        extensions: &["rs"],
70        filenames: &[],
71        interpreters: &[],
72        aliases: &["rust", "rs"],
73        highlights: include_str!("../queries/rust/highlights.scm"),
74        injections: "",
75    },
76    Entry {
77        id: LanguageId::Python,
78        name: "python",
79        grammar: || tree_sitter_python::LANGUAGE.into(),
80        extensions: &["py", "pyi", "pyw"],
81        filenames: &[],
82        interpreters: &["python", "python3"],
83        aliases: &["python", "py", "python3"],
84        highlights: include_str!("../queries/python/highlights.scm"),
85        injections: "",
86    },
87    Entry {
88        id: LanguageId::JavaScript,
89        name: "javascript",
90        grammar: || tree_sitter_javascript::LANGUAGE.into(),
91        extensions: &["js", "jsx", "mjs", "cjs"],
92        filenames: &[],
93        interpreters: &["node", "nodejs"],
94        aliases: &["javascript", "js", "node"],
95        highlights: include_str!("../queries/javascript/highlights.scm"),
96        injections: "",
97    },
98    Entry {
99        id: LanguageId::TypeScript,
100        name: "typescript",
101        grammar: || tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
102        extensions: &["ts", "mts", "cts"],
103        filenames: &[],
104        interpreters: &[],
105        aliases: &["typescript", "ts"],
106        highlights: include_str!("../queries/typescript/highlights.scm"),
107        injections: "",
108    },
109    Entry {
110        id: LanguageId::Tsx,
111        name: "tsx",
112        grammar: || tree_sitter_typescript::LANGUAGE_TSX.into(),
113        extensions: &["tsx"],
114        filenames: &[],
115        interpreters: &[],
116        aliases: &["tsx"],
117        highlights: include_str!("../queries/tsx/highlights.scm"),
118        injections: "",
119    },
120    Entry {
121        id: LanguageId::Go,
122        name: "go",
123        grammar: || tree_sitter_go::LANGUAGE.into(),
124        extensions: &["go"],
125        filenames: &[],
126        interpreters: &[],
127        aliases: &["go", "golang"],
128        highlights: include_str!("../queries/go/highlights.scm"),
129        injections: "",
130    },
131    Entry {
132        id: LanguageId::C,
133        name: "c",
134        grammar: || tree_sitter_c::LANGUAGE.into(),
135        extensions: &["c", "h"],
136        filenames: &[],
137        interpreters: &[],
138        aliases: &["c"],
139        highlights: include_str!("../queries/c/highlights.scm"),
140        injections: "",
141    },
142    Entry {
143        id: LanguageId::Cpp,
144        name: "cpp",
145        grammar: || tree_sitter_cpp::LANGUAGE.into(),
146        extensions: &["cpp", "cc", "cp", "cxx", "hpp", "hh", "hxx", "ino", "tpp"],
147        filenames: &[],
148        interpreters: &[],
149        aliases: &["cpp", "c++", "cxx"],
150        highlights: include_str!("../queries/cpp/highlights.scm"),
151        injections: "",
152    },
153    Entry {
154        id: LanguageId::Json,
155        name: "json",
156        grammar: || tree_sitter_json::LANGUAGE.into(),
157        extensions: &["json", "jsonc"],
158        filenames: &[],
159        interpreters: &[],
160        aliases: &["json", "jsonc"],
161        highlights: include_str!("../queries/json/highlights.scm"),
162        injections: "",
163    },
164    Entry {
165        id: LanguageId::Bash,
166        name: "bash",
167        grammar: || tree_sitter_bash::LANGUAGE.into(),
168        extensions: &["sh", "bash", "zsh", "ksh"],
169        filenames: &[
170            ".bashrc",
171            ".bash_profile",
172            ".bash_aliases",
173            ".bash_logout",
174            ".zshrc",
175            ".zshenv",
176            ".zprofile",
177            ".profile",
178            "PKGBUILD",
179            "APKBUILD",
180        ],
181        interpreters: &["bash", "sh", "dash", "zsh", "ksh"],
182        aliases: &["bash", "sh", "shell", "zsh", "shell-script"],
183        highlights: include_str!("../queries/bash/highlights.scm"),
184        injections: "",
185    },
186    Entry {
187        id: LanguageId::Fish,
188        name: "fish",
189        grammar: tree_sitter_fish::language,
190        extensions: &["fish"],
191        filenames: &[],
192        interpreters: &["fish"],
193        aliases: &["fish"],
194        highlights: include_str!("../queries/fish/highlights.scm"),
195        injections: "",
196    },
197    Entry {
198        id: LanguageId::Lua,
199        name: "lua",
200        grammar: || tree_sitter_lua::LANGUAGE.into(),
201        extensions: &["lua"],
202        filenames: &[],
203        interpreters: &["lua"],
204        aliases: &["lua"],
205        highlights: include_str!("../queries/lua/highlights.scm"),
206        injections: "",
207    },
208    Entry {
209        id: LanguageId::Sql,
210        name: "sql",
211        grammar: || tree_sitter_sequel::LANGUAGE.into(),
212        extensions: &["sql"],
213        filenames: &[],
214        interpreters: &[],
215        aliases: &["sql"],
216        highlights: include_str!("../queries/sql/highlights.scm"),
217        injections: "",
218    },
219    Entry {
220        id: LanguageId::CMake,
221        name: "cmake",
222        grammar: || tree_sitter_cmake::LANGUAGE.into(),
223        extensions: &["cmake"],
224        filenames: &["CMakeLists.txt"],
225        interpreters: &["cmake"],
226        aliases: &["cmake"],
227        highlights: include_str!("../queries/cmake/highlights.scm"),
228        injections: "",
229    },
230    Entry {
231        id: LanguageId::Markdown,
232        name: "markdown",
233        grammar: || tree_sitter_md::LANGUAGE.into(),
234        extensions: &["md", "markdown"],
235        filenames: &[],
236        interpreters: &[],
237        aliases: &["markdown", "md"],
238        highlights: include_str!("../queries/markdown/highlights.scm"),
239        injections: include_str!("../queries/markdown/injections.scm"),
240    },
241    Entry {
242        id: LanguageId::MarkdownInline,
243        name: "markdown.inline",
244        grammar: || tree_sitter_md::INLINE_LANGUAGE.into(),
245        extensions: &[],
246        filenames: &[],
247        interpreters: &[],
248        aliases: &["markdown.inline", "markdown_inline"],
249        highlights: include_str!("../queries/markdown.inline/highlights.scm"),
250        injections: include_str!("../queries/markdown.inline/injections.scm"),
251    },
252    Entry {
253        id: LanguageId::Java,
254        name: "java",
255        grammar: || tree_sitter_java::LANGUAGE.into(),
256        extensions: &["java"],
257        filenames: &[],
258        interpreters: &[],
259        aliases: &["java"],
260        highlights: include_str!("../queries/java/highlights.scm"),
261        injections: "",
262    },
263    Entry {
264        id: LanguageId::CSharp,
265        name: "c-sharp",
266        grammar: || tree_sitter_c_sharp::LANGUAGE.into(),
267        extensions: &["cs", "csx"],
268        filenames: &[],
269        interpreters: &[],
270        aliases: &["csharp", "c#", "cs", "c-sharp"],
271        highlights: include_str!("../queries/c-sharp/highlights.scm"),
272        injections: "",
273    },
274    Entry {
275        id: LanguageId::Ruby,
276        name: "ruby",
277        grammar: || tree_sitter_ruby::LANGUAGE.into(),
278        extensions: &["rb", "rbw", "rake", "gemspec"],
279        filenames: &["Gemfile", "Rakefile", "Vagrantfile", "Guardfile"],
280        interpreters: &["ruby"],
281        aliases: &["ruby", "rb"],
282        highlights: include_str!("../queries/ruby/highlights.scm"),
283        injections: "",
284    },
285    Entry {
286        id: LanguageId::Php,
287        name: "php",
288        grammar: || tree_sitter_php::LANGUAGE_PHP.into(),
289        extensions: &["php", "phtml"],
290        filenames: &[],
291        interpreters: &["php"],
292        aliases: &["php"],
293        highlights: include_str!("../queries/php/highlights.scm"),
294        injections: include_str!("../queries/php/injections.scm"),
295    },
296    Entry {
297        id: LanguageId::Toml,
298        name: "toml",
299        grammar: || tree_sitter_toml_ng::LANGUAGE.into(),
300        extensions: &["toml"],
301        filenames: &[],
302        interpreters: &[],
303        aliases: &["toml"],
304        highlights: include_str!("../queries/toml/highlights.scm"),
305        injections: "",
306    },
307    Entry {
308        id: LanguageId::Yaml,
309        name: "yaml",
310        grammar: || tree_sitter_yaml::LANGUAGE.into(),
311        extensions: &["yaml", "yml"],
312        filenames: &[],
313        interpreters: &[],
314        aliases: &["yaml", "yml"],
315        highlights: include_str!("../queries/yaml/highlights.scm"),
316        injections: "",
317    },
318    Entry {
319        id: LanguageId::Html,
320        name: "html",
321        grammar: || tree_sitter_html::LANGUAGE.into(),
322        extensions: &["html", "htm", "xhtml"],
323        filenames: &[],
324        interpreters: &[],
325        aliases: &["html"],
326        highlights: include_str!("../queries/html/highlights.scm"),
327        injections: include_str!("../queries/html/injections.scm"),
328    },
329    Entry {
330        id: LanguageId::Css,
331        name: "css",
332        grammar: || tree_sitter_css::LANGUAGE.into(),
333        extensions: &["css"],
334        filenames: &[],
335        interpreters: &[],
336        aliases: &["css"],
337        highlights: include_str!("../queries/css/highlights.scm"),
338        injections: "",
339    },
340];
341
342pub fn for_extension(extension: &str) -> Option<LanguageSpec> {
343    let extension = extension.strip_prefix('.').unwrap_or(extension);
344    LANGUAGES
345        .iter()
346        .find(|entry| {
347            entry
348                .extensions
349                .iter()
350                .any(|candidate| candidate.eq_ignore_ascii_case(extension))
351        })
352        .map(Entry::spec)
353}
354
355pub fn for_name(name: &str) -> Option<LanguageSpec> {
356    let name = name.trim();
357    let name = name
358        .strip_prefix("source.")
359        .or_else(|| name.strip_prefix("text."))
360        .unwrap_or(name);
361    LANGUAGES
362        .iter()
363        .find(|entry| {
364            entry
365                .aliases
366                .iter()
367                .any(|alias| alias.eq_ignore_ascii_case(name))
368        })
369        .map(Entry::spec)
370}
371
372/// A registry walk for compatibility checks, not another detection vocabulary.
373pub fn specifications() -> impl Iterator<Item = LanguageSpec> {
374    LANGUAGES.iter().map(Entry::spec)
375}
376
377pub fn interpreter_of(first_line: &str) -> Option<&str> {
378    let mut tokens = first_line.strip_prefix("#!")?.split_whitespace();
379    let program = tokens.next()?;
380    let program = if basename(program) == Some("env") {
381        tokens.find(|token| !token.starts_with('-'))?
382    } else {
383        program
384    };
385    basename(program)
386}
387fn basename(program: &str) -> Option<&str> {
388    Path::new(program)
389        .file_name()
390        .and_then(|name| name.to_str())
391        .filter(|name| !name.is_empty())
392}
393pub fn for_shebang(first_line: &str) -> Option<LanguageSpec> {
394    let interpreter = interpreter_of(first_line)?;
395    LANGUAGES
396        .iter()
397        .find(|entry| entry.interpreters.contains(&interpreter))
398        .map(Entry::spec)
399}
400
401/// Exact basename wins over extension, which wins over the bounded shebang.
402/// Non-UTF-8 filename bytes never become a lossy path used for I/O.
403pub fn detect(path: &Path, first_line: Option<&str>) -> Option<LanguageSpec> {
404    if let Some(name) = path.file_name().and_then(|name| name.to_str()) {
405        if let Some(entry) = LANGUAGES.iter().find(|entry| {
406            entry
407                .filenames
408                .iter()
409                .any(|filename| filename.eq_ignore_ascii_case(name))
410        }) {
411            return Some(entry.spec());
412        }
413    }
414    path.extension()
415        .and_then(|extension| extension.to_str())
416        .and_then(for_extension)
417        .or_else(|| first_line.and_then(for_shebang))
418}
419
420#[cfg(test)]
421mod tests;