1use tree_sitter::Language as TsLanguage;
27
28#[allow(missing_docs)]
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum Language {
38 Harn,
39 TypeScript,
40 Tsx,
41 JavaScript,
42 Jsx,
43 Python,
44 Go,
45 Rust,
46 Java,
47 C,
48 Cpp,
49 CSharp,
50 Ruby,
51 Kotlin,
52 Php,
53 Scala,
54 Bash,
55 Swift,
56 Zig,
57 Elixir,
58 Lua,
59 Haskell,
60 R,
61 Json,
62 Yaml,
63 Toml,
64 Css,
65 Html,
66 Sql,
67 Markdown,
68}
69
70pub const TEXT_PATCH_FALLBACK: &str =
75 "fall back to a text-level edit (std/edit `edit_safe_text_patch`)";
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct EditCapabilities {
88 pub apply_node: bool,
90 pub insert_at_anchor: bool,
92 pub rename_symbol: bool,
94 pub symbols: bool,
96}
97
98impl Language {
99 pub fn name(self) -> &'static str {
101 match self {
102 Language::Harn => "harn",
103 Language::TypeScript => "typescript",
104 Language::Tsx => "tsx",
105 Language::JavaScript => "javascript",
106 Language::Jsx => "jsx",
107 Language::Python => "python",
108 Language::Go => "go",
109 Language::Rust => "rust",
110 Language::Java => "java",
111 Language::C => "c",
112 Language::Cpp => "cpp",
113 Language::CSharp => "csharp",
114 Language::Ruby => "ruby",
115 Language::Kotlin => "kotlin",
116 Language::Php => "php",
117 Language::Scala => "scala",
118 Language::Bash => "bash",
119 Language::Swift => "swift",
120 Language::Zig => "zig",
121 Language::Elixir => "elixir",
122 Language::Lua => "lua",
123 Language::Haskell => "haskell",
124 Language::R => "r",
125 Language::Json => "json",
126 Language::Yaml => "yaml",
127 Language::Toml => "toml",
128 Language::Css => "css",
129 Language::Html => "html",
130 Language::Sql => "sql",
131 Language::Markdown => "markdown",
132 }
133 }
134
135 pub fn ts_language(self) -> Option<TsLanguage> {
146 match self {
151 #[cfg(feature = "grammar-harn")]
152 Language::Harn => Some(tree_sitter_harn::LANGUAGE.into()),
153
154 #[cfg(feature = "grammar-web")]
155 Language::TypeScript => Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
156 #[cfg(feature = "grammar-web")]
157 Language::Tsx => Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
158 #[cfg(feature = "grammar-web")]
159 Language::JavaScript | Language::Jsx => Some(tree_sitter_javascript::LANGUAGE.into()),
160 #[cfg(feature = "grammar-web")]
161 Language::Html => Some(tree_sitter_html::LANGUAGE.into()),
162 #[cfg(feature = "grammar-web")]
163 Language::Css => Some(tree_sitter_css::LANGUAGE.into()),
164
165 #[cfg(feature = "grammar-systems")]
166 Language::Rust => Some(tree_sitter_rust::LANGUAGE.into()),
167 #[cfg(feature = "grammar-systems")]
168 Language::C => Some(tree_sitter_c::LANGUAGE.into()),
169 #[cfg(feature = "grammar-systems")]
170 Language::Cpp => Some(tree_sitter_cpp::LANGUAGE.into()),
171 #[cfg(feature = "grammar-systems")]
172 Language::Go => Some(tree_sitter_go::LANGUAGE.into()),
173 #[cfg(feature = "grammar-systems")]
174 Language::Zig => Some(tree_sitter_zig::LANGUAGE.into()),
175
176 #[cfg(feature = "grammar-scripting")]
177 Language::Python => Some(tree_sitter_python::LANGUAGE.into()),
178 #[cfg(feature = "grammar-scripting")]
179 Language::Ruby => Some(tree_sitter_ruby::LANGUAGE.into()),
180 #[cfg(feature = "grammar-scripting")]
181 Language::Bash => Some(tree_sitter_bash::LANGUAGE.into()),
182 #[cfg(feature = "grammar-scripting")]
183 Language::Lua => Some(tree_sitter_lua::LANGUAGE.into()),
184 #[cfg(feature = "grammar-scripting")]
185 Language::Php => Some(tree_sitter_php::LANGUAGE_PHP.into()),
186 #[cfg(feature = "grammar-scripting")]
187 Language::R => Some(tree_sitter_r::LANGUAGE.into()),
188
189 #[cfg(feature = "grammar-jvm")]
190 Language::Java => Some(tree_sitter_java::LANGUAGE.into()),
191 #[cfg(feature = "grammar-jvm")]
192 Language::Kotlin => Some(tree_sitter_kotlin_ng::LANGUAGE.into()),
193 #[cfg(feature = "grammar-jvm")]
194 Language::Scala => Some(tree_sitter_scala::LANGUAGE.into()),
195
196 #[cfg(feature = "grammar-enterprise")]
197 Language::CSharp => Some(tree_sitter_c_sharp::LANGUAGE.into()),
198 #[cfg(feature = "grammar-enterprise")]
199 Language::Swift => Some(tree_sitter_swift::LANGUAGE.into()),
200 #[cfg(feature = "grammar-enterprise")]
201 Language::Elixir => Some(tree_sitter_elixir::LANGUAGE.into()),
202 #[cfg(feature = "grammar-enterprise")]
203 Language::Haskell => Some(tree_sitter_haskell::LANGUAGE.into()),
204
205 #[cfg(feature = "grammar-data")]
206 Language::Json => Some(tree_sitter_json::LANGUAGE.into()),
207 #[cfg(feature = "grammar-data")]
208 Language::Yaml => Some(tree_sitter_yaml::LANGUAGE.into()),
209 #[cfg(feature = "grammar-data")]
210 Language::Toml => Some(tree_sitter_toml_ng::LANGUAGE.into()),
211 #[cfg(feature = "grammar-data")]
212 Language::Sql => Some(tree_sitter_sequel::LANGUAGE.into()),
213 #[cfg(feature = "grammar-data")]
217 Language::Markdown => Some(tree_sitter_md::LANGUAGE.into()),
218
219 #[allow(unreachable_patterns)]
222 _ => None,
223 }
224 }
225
226 pub fn from_name(name: &str) -> Option<Self> {
230 let normalized = name.trim().to_ascii_lowercase();
231 Some(match normalized.as_str() {
232 "harn" => Language::Harn,
233 "typescript" | "ts" => Language::TypeScript,
234 "tsx" => Language::Tsx,
235 "javascript" | "js" => Language::JavaScript,
236 "jsx" => Language::Jsx,
237 "python" | "py" => Language::Python,
238 "go" | "golang" => Language::Go,
239 "rust" | "rs" => Language::Rust,
240 "java" => Language::Java,
241 "c" => Language::C,
242 "cpp" | "c++" | "cxx" => Language::Cpp,
243 "csharp" | "c#" | "cs" => Language::CSharp,
244 "ruby" | "rb" => Language::Ruby,
245 "kotlin" | "kt" => Language::Kotlin,
246 "php" => Language::Php,
247 "scala" => Language::Scala,
248 "bash" | "shell" | "sh" | "zsh" => Language::Bash,
249 "swift" => Language::Swift,
250 "zig" => Language::Zig,
251 "elixir" | "ex" => Language::Elixir,
252 "lua" => Language::Lua,
253 "haskell" | "hs" => Language::Haskell,
254 "r" => Language::R,
255 "json" => Language::Json,
256 "yaml" | "yml" => Language::Yaml,
257 "toml" => Language::Toml,
258 "css" => Language::Css,
259 "html" | "htm" => Language::Html,
260 "sql" => Language::Sql,
261 "markdown" | "md" => Language::Markdown,
262 _ => return None,
263 })
264 }
265
266 pub fn from_extension(ext: &str) -> Option<Self> {
268 let normalized = ext.trim_start_matches('.').to_ascii_lowercase();
269 Some(match normalized.as_str() {
270 "harn" => Language::Harn,
271 "ts" => Language::TypeScript,
272 "tsx" => Language::Tsx,
273 "js" | "mjs" | "cjs" => Language::JavaScript,
274 "jsx" => Language::Jsx,
275 "py" => Language::Python,
276 "go" => Language::Go,
277 "rs" => Language::Rust,
278 "java" => Language::Java,
279 "c" | "h" => Language::C,
280 "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => Language::Cpp,
281 "cs" | "csx" => Language::CSharp,
282 "rb" => Language::Ruby,
283 "kt" | "kts" => Language::Kotlin,
284 "php" => Language::Php,
285 "scala" | "sc" => Language::Scala,
286 "sh" | "bash" | "zsh" => Language::Bash,
287 "swift" => Language::Swift,
288 "zig" | "zon" => Language::Zig,
289 "ex" | "exs" => Language::Elixir,
290 "lua" => Language::Lua,
291 "hs" | "lhs" => Language::Haskell,
292 "r" => Language::R,
293 "json" => Language::Json,
294 "yaml" | "yml" => Language::Yaml,
295 "toml" => Language::Toml,
296 "css" => Language::Css,
297 "html" | "htm" => Language::Html,
298 "sql" => Language::Sql,
299 "md" | "markdown" => Language::Markdown,
300 _ => return None,
301 })
302 }
303
304 pub fn detect(path: &std::path::Path, language_hint: Option<&str>) -> Option<Self> {
307 if let Some(name) = language_hint.and_then(|s| (!s.is_empty()).then_some(s)) {
308 return Self::from_name(name);
309 }
310 let ext = path.extension().and_then(|s| s.to_str())?;
311 Self::from_extension(ext)
312 }
313
314 pub fn primary_extension(self) -> &'static str {
318 match self {
319 Language::Harn => "harn",
320 Language::TypeScript => "ts",
321 Language::Tsx => "tsx",
322 Language::JavaScript => "js",
323 Language::Jsx => "jsx",
324 Language::Python => "py",
325 Language::Go => "go",
326 Language::Rust => "rs",
327 Language::Java => "java",
328 Language::C => "c",
329 Language::Cpp => "cpp",
330 Language::CSharp => "cs",
331 Language::Ruby => "rb",
332 Language::Kotlin => "kt",
333 Language::Php => "php",
334 Language::Scala => "scala",
335 Language::Bash => "sh",
336 Language::Swift => "swift",
337 Language::Zig => "zig",
338 Language::Elixir => "ex",
339 Language::Lua => "lua",
340 Language::Haskell => "hs",
341 Language::R => "r",
342 Language::Json => "json",
343 Language::Yaml => "yaml",
344 Language::Toml => "toml",
345 Language::Css => "css",
346 Language::Html => "html",
347 Language::Sql => "sql",
348 Language::Markdown => "md",
349 }
350 }
351
352 pub fn rename_identifier_kinds(self) -> Option<&'static [&'static str]> {
360 Some(match self {
361 Language::Harn => &["identifier"],
362 Language::Rust => &[
363 "identifier",
364 "type_identifier",
365 "field_identifier",
366 "shorthand_field_identifier",
367 ],
368 Language::TypeScript | Language::Tsx => &[
369 "identifier",
370 "type_identifier",
371 "property_identifier",
372 "shorthand_property_identifier",
373 "shorthand_property_identifier_pattern",
374 ],
375 Language::JavaScript | Language::Jsx => &[
376 "identifier",
377 "property_identifier",
378 "shorthand_property_identifier",
379 "shorthand_property_identifier_pattern",
380 ],
381 Language::Python => &["identifier"],
382 Language::Go => &[
383 "identifier",
384 "type_identifier",
385 "field_identifier",
386 "package_identifier",
387 ],
388 Language::Swift => &["simple_identifier", "type_identifier"],
389 _ => return None,
390 })
391 }
392
393 pub fn supports_rename(self) -> bool {
396 self.rename_identifier_kinds().is_some()
397 }
398
399 fn is_data_format(self) -> bool {
402 matches!(
403 self,
404 Language::Json
405 | Language::Yaml
406 | Language::Toml
407 | Language::Css
408 | Language::Html
409 | Language::Sql
410 | Language::Markdown
411 )
412 }
413
414 pub fn supports_symbol_extraction(self) -> bool {
417 !self.is_data_format()
418 }
419
420 pub fn edit_capabilities(self) -> EditCapabilities {
423 EditCapabilities {
424 apply_node: true,
425 insert_at_anchor: true,
426 rename_symbol: self.supports_rename(),
427 symbols: self.supports_symbol_extraction(),
428 }
429 }
430
431 pub fn all() -> &'static [Language] {
433 &[
434 Language::Harn,
435 Language::TypeScript,
436 Language::Tsx,
437 Language::JavaScript,
438 Language::Jsx,
439 Language::Python,
440 Language::Go,
441 Language::Rust,
442 Language::Java,
443 Language::C,
444 Language::Cpp,
445 Language::CSharp,
446 Language::Ruby,
447 Language::Kotlin,
448 Language::Php,
449 Language::Scala,
450 Language::Bash,
451 Language::Swift,
452 Language::Zig,
453 Language::Elixir,
454 Language::Lua,
455 Language::Haskell,
456 Language::R,
457 Language::Json,
458 Language::Yaml,
459 Language::Toml,
460 Language::Css,
461 Language::Html,
462 Language::Sql,
463 Language::Markdown,
464 ]
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[cfg(feature = "grammars-all")]
475 #[test]
476 fn every_language_is_loadable() {
477 for &lang in Language::all() {
478 let ts = lang
481 .ts_language()
482 .unwrap_or_else(|| panic!("{} grammar not compiled", lang.name()));
483 assert!(ts.node_kind_count() > 0, "{} grammar is empty", lang.name());
484 }
485 }
486
487 #[test]
488 fn extension_detection_round_trips_canonical_extensions() {
489 let cases: &[(&str, Language)] = &[
490 ("harn", Language::Harn),
491 ("ts", Language::TypeScript),
492 ("tsx", Language::Tsx),
493 ("js", Language::JavaScript),
494 ("jsx", Language::Jsx),
495 ("py", Language::Python),
496 ("rs", Language::Rust),
497 ("go", Language::Go),
498 ("java", Language::Java),
499 ("c", Language::C),
500 ("cpp", Language::Cpp),
501 ("cs", Language::CSharp),
502 ("rb", Language::Ruby),
503 ("kt", Language::Kotlin),
504 ("php", Language::Php),
505 ("scala", Language::Scala),
506 ("sh", Language::Bash),
507 ("swift", Language::Swift),
508 ("zig", Language::Zig),
509 ("ex", Language::Elixir),
510 ("lua", Language::Lua),
511 ("hs", Language::Haskell),
512 ("r", Language::R),
513 ("json", Language::Json),
514 ("yaml", Language::Yaml),
515 ("yml", Language::Yaml),
516 ("toml", Language::Toml),
517 ("css", Language::Css),
518 ("html", Language::Html),
519 ("sql", Language::Sql),
520 ("md", Language::Markdown),
521 ];
522 for (ext, want) in cases {
523 assert_eq!(Language::from_extension(ext), Some(*want), "ext {ext}");
524 }
525 }
526
527 #[test]
528 fn name_round_trips_for_every_language() {
529 for &lang in Language::all() {
530 assert_eq!(Language::from_name(lang.name()), Some(lang));
531 }
532 }
533
534 #[test]
535 fn primary_extension_resolves_back_to_the_language() {
536 for &lang in Language::all() {
537 assert_eq!(
538 Language::from_extension(lang.primary_extension()),
539 Some(lang),
540 "primary extension for {} does not round-trip",
541 lang.name()
542 );
543 }
544 }
545
546 #[test]
547 fn detect_prefers_hint_over_extension() {
548 let path = std::path::Path::new("foo.ts");
549 assert_eq!(Language::detect(path, None), Some(Language::TypeScript));
550 assert_eq!(
551 Language::detect(path, Some("javascript")),
552 Some(Language::JavaScript)
553 );
554 }
555
556 #[test]
557 fn edit_primitives_are_universal_rename_is_gated() {
558 for &lang in Language::all() {
559 let caps = lang.edit_capabilities();
560 assert!(caps.apply_node, "{} should support apply_node", lang.name());
561 assert!(
562 caps.insert_at_anchor,
563 "{} should support insert_at_anchor",
564 lang.name()
565 );
566 assert_eq!(
567 caps.rename_symbol,
568 lang.rename_identifier_kinds().is_some(),
569 "{} rename capability must match its identifier-kind table",
570 lang.name()
571 );
572 }
573 assert!(!Language::Json.edit_capabilities().rename_symbol);
575 assert!(!Language::Json.edit_capabilities().symbols);
576 assert!(Language::Rust.edit_capabilities().rename_symbol);
577 assert!(Language::Rust.edit_capabilities().symbols);
578 }
579}