1use 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
372pub 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
401pub 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;