use harn_languages::Language as RegistryLanguage;
use harn_languages::{LanguageClass, StructuralSupport};
use tree_sitter::Language as TsLanguage;
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Language {
Harn,
TypeScript,
Tsx,
JavaScript,
Jsx,
Python,
Go,
Rust,
Java,
C,
Cpp,
CSharp,
Ruby,
Kotlin,
Php,
Scala,
Bash,
Swift,
Zig,
Elixir,
Lua,
Haskell,
R,
Json,
Yaml,
Toml,
Css,
Html,
Sql,
Markdown,
}
pub const TEXT_PATCH_FALLBACK: &str =
"fall back to a text-level edit (std/edit `edit_safe_text_patch`)";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct EditCapabilities {
pub apply_node: bool,
pub insert_at_anchor: bool,
pub rename_symbol: bool,
pub symbols: bool,
pub move_symbol: bool,
pub extract_function: bool,
pub change_signature: bool,
}
impl Language {
pub fn registry(self) -> RegistryLanguage {
match self {
Language::Harn => RegistryLanguage::Harn,
Language::TypeScript => RegistryLanguage::TypeScript,
Language::Tsx => RegistryLanguage::Tsx,
Language::JavaScript => RegistryLanguage::JavaScript,
Language::Jsx => RegistryLanguage::Jsx,
Language::Python => RegistryLanguage::Python,
Language::Go => RegistryLanguage::Go,
Language::Rust => RegistryLanguage::Rust,
Language::Java => RegistryLanguage::Java,
Language::C => RegistryLanguage::C,
Language::Cpp => RegistryLanguage::Cpp,
Language::CSharp => RegistryLanguage::CSharp,
Language::Ruby => RegistryLanguage::Ruby,
Language::Kotlin => RegistryLanguage::Kotlin,
Language::Php => RegistryLanguage::Php,
Language::Scala => RegistryLanguage::Scala,
Language::Bash => RegistryLanguage::Bash,
Language::Swift => RegistryLanguage::Swift,
Language::Zig => RegistryLanguage::Zig,
Language::Elixir => RegistryLanguage::Elixir,
Language::Lua => RegistryLanguage::Lua,
Language::Haskell => RegistryLanguage::Haskell,
Language::R => RegistryLanguage::R,
Language::Json => RegistryLanguage::Json,
Language::Yaml => RegistryLanguage::Yaml,
Language::Toml => RegistryLanguage::Toml,
Language::Css => RegistryLanguage::Css,
Language::Html => RegistryLanguage::Html,
Language::Sql => RegistryLanguage::Sql,
Language::Markdown => RegistryLanguage::Markdown,
}
}
pub fn from_registry(language: RegistryLanguage) -> Option<Self> {
Self::all()
.iter()
.copied()
.find(|lang| lang.registry() == language)
}
pub fn name(self) -> &'static str {
self.registry().name()
}
pub fn ts_language(self) -> Option<TsLanguage> {
match self {
#[cfg(feature = "grammar-harn")]
Language::Harn => Some(tree_sitter_harn::LANGUAGE.into()),
#[cfg(feature = "grammar-web")]
Language::TypeScript => Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
#[cfg(feature = "grammar-web")]
Language::Tsx => Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
#[cfg(feature = "grammar-web")]
Language::JavaScript | Language::Jsx => Some(tree_sitter_javascript::LANGUAGE.into()),
#[cfg(feature = "grammar-web")]
Language::Html => Some(tree_sitter_html::LANGUAGE.into()),
#[cfg(feature = "grammar-web")]
Language::Css => Some(tree_sitter_css::LANGUAGE.into()),
#[cfg(feature = "grammar-systems")]
Language::Rust => Some(tree_sitter_rust::LANGUAGE.into()),
#[cfg(feature = "grammar-systems")]
Language::C => Some(tree_sitter_c::LANGUAGE.into()),
#[cfg(feature = "grammar-systems")]
Language::Cpp => Some(tree_sitter_cpp::LANGUAGE.into()),
#[cfg(feature = "grammar-systems")]
Language::Go => Some(tree_sitter_go::LANGUAGE.into()),
#[cfg(feature = "grammar-systems")]
Language::Zig => Some(tree_sitter_zig::LANGUAGE.into()),
#[cfg(feature = "grammar-scripting")]
Language::Python => Some(tree_sitter_python::LANGUAGE.into()),
#[cfg(feature = "grammar-scripting")]
Language::Ruby => Some(tree_sitter_ruby::LANGUAGE.into()),
#[cfg(feature = "grammar-scripting")]
Language::Bash => Some(tree_sitter_bash::LANGUAGE.into()),
#[cfg(feature = "grammar-scripting")]
Language::Lua => Some(tree_sitter_lua::LANGUAGE.into()),
#[cfg(feature = "grammar-scripting")]
Language::Php => Some(tree_sitter_php::LANGUAGE_PHP.into()),
#[cfg(feature = "grammar-scripting")]
Language::R => Some(tree_sitter_r::LANGUAGE.into()),
#[cfg(feature = "grammar-jvm")]
Language::Java => Some(tree_sitter_java::LANGUAGE.into()),
#[cfg(feature = "grammar-jvm")]
Language::Kotlin => Some(tree_sitter_kotlin_ng::LANGUAGE.into()),
#[cfg(feature = "grammar-jvm")]
Language::Scala => Some(tree_sitter_scala::LANGUAGE.into()),
#[cfg(feature = "grammar-enterprise")]
Language::CSharp => Some(tree_sitter_c_sharp::LANGUAGE.into()),
#[cfg(feature = "grammar-enterprise")]
Language::Swift => Some(tree_sitter_swift::LANGUAGE.into()),
#[cfg(feature = "grammar-enterprise")]
Language::Elixir => Some(tree_sitter_elixir::LANGUAGE.into()),
#[cfg(feature = "grammar-enterprise")]
Language::Haskell => Some(tree_sitter_haskell::LANGUAGE.into()),
#[cfg(feature = "grammar-data")]
Language::Json => Some(tree_sitter_json::LANGUAGE.into()),
#[cfg(feature = "grammar-data")]
Language::Yaml => Some(tree_sitter_yaml::LANGUAGE.into()),
#[cfg(feature = "grammar-data")]
Language::Toml => Some(tree_sitter_toml_ng::LANGUAGE.into()),
#[cfg(feature = "grammar-data")]
Language::Sql => Some(tree_sitter_sequel::LANGUAGE.into()),
#[cfg(feature = "grammar-data")]
Language::Markdown => Some(tree_sitter_md::LANGUAGE.into()),
#[allow(unreachable_patterns)]
_ => None,
}
}
pub fn from_name(name: &str) -> Option<Self> {
RegistryLanguage::from_name(name).and_then(Self::from_registry)
}
pub fn from_extension(ext: &str) -> Option<Self> {
RegistryLanguage::from_extension(ext).and_then(Self::from_registry)
}
pub fn detect(path: &std::path::Path, language_hint: Option<&str>) -> Option<Self> {
if let Some(name) = language_hint.and_then(|s| (!s.is_empty()).then_some(s)) {
return Self::from_name(name);
}
RegistryLanguage::from_path(path).and_then(Self::from_registry)
}
pub fn primary_extension(self) -> &'static str {
self.registry().extensions()[0]
}
pub fn rename_identifier_kinds(self) -> Option<&'static [&'static str]> {
Some(match self {
Language::Harn => &["identifier"],
Language::Rust => &[
"identifier",
"type_identifier",
"field_identifier",
"shorthand_field_identifier",
],
Language::TypeScript | Language::Tsx => &[
"identifier",
"type_identifier",
"property_identifier",
"shorthand_property_identifier",
"shorthand_property_identifier_pattern",
],
Language::JavaScript | Language::Jsx => &[
"identifier",
"property_identifier",
"shorthand_property_identifier",
"shorthand_property_identifier_pattern",
],
Language::Python => &["identifier"],
Language::Go => &[
"identifier",
"type_identifier",
"field_identifier",
"package_identifier",
],
Language::Swift => &["simple_identifier", "type_identifier"],
_ => return None,
})
}
fn structural(self) -> StructuralSupport {
self.registry()
.structural()
.expect("every grammar-backed language has a structural registry row")
}
pub fn supports_rename(self) -> bool {
self.structural().rename_symbol
}
pub fn supports_change_signature(self) -> bool {
self.structural().change_signature
}
pub fn supports_extract_function(self) -> bool {
self.structural().extract_function
}
pub fn supports_move_symbol(self) -> bool {
self.structural().move_symbol
}
pub fn supports_symbol_extraction(self) -> bool {
self.registry().class() == LanguageClass::Code
}
pub fn edit_capabilities(self) -> EditCapabilities {
EditCapabilities {
apply_node: true,
insert_at_anchor: true,
rename_symbol: self.supports_rename(),
symbols: self.supports_symbol_extraction(),
move_symbol: self.supports_move_symbol(),
extract_function: self.supports_extract_function(),
change_signature: self.supports_change_signature(),
}
}
pub fn all() -> &'static [Language] {
&[
Language::Harn,
Language::TypeScript,
Language::Tsx,
Language::JavaScript,
Language::Jsx,
Language::Python,
Language::Go,
Language::Rust,
Language::Java,
Language::C,
Language::Cpp,
Language::CSharp,
Language::Ruby,
Language::Kotlin,
Language::Php,
Language::Scala,
Language::Bash,
Language::Swift,
Language::Zig,
Language::Elixir,
Language::Lua,
Language::Haskell,
Language::R,
Language::Json,
Language::Yaml,
Language::Toml,
Language::Css,
Language::Html,
Language::Sql,
Language::Markdown,
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "grammars-all")]
#[test]
fn every_language_is_loadable() {
for &lang in Language::all() {
let ts = lang
.ts_language()
.unwrap_or_else(|| panic!("{} grammar not compiled", lang.name()));
assert!(ts.node_kind_count() > 0, "{} grammar is empty", lang.name());
}
}
#[test]
fn extension_detection_round_trips_canonical_extensions() {
let cases: &[(&str, Language)] = &[
("harn", Language::Harn),
("ts", Language::TypeScript),
("tsx", Language::Tsx),
("js", Language::JavaScript),
("jsx", Language::Jsx),
("py", Language::Python),
("rs", Language::Rust),
("go", Language::Go),
("java", Language::Java),
("c", Language::C),
("cpp", Language::Cpp),
("cs", Language::CSharp),
("rb", Language::Ruby),
("kt", Language::Kotlin),
("php", Language::Php),
("scala", Language::Scala),
("sh", Language::Bash),
("swift", Language::Swift),
("zig", Language::Zig),
("ex", Language::Elixir),
("lua", Language::Lua),
("hs", Language::Haskell),
("r", Language::R),
("json", Language::Json),
("yaml", Language::Yaml),
("yml", Language::Yaml),
("toml", Language::Toml),
("css", Language::Css),
("html", Language::Html),
("sql", Language::Sql),
("md", Language::Markdown),
];
for (ext, want) in cases {
assert_eq!(Language::from_extension(ext), Some(*want), "ext {ext}");
}
}
#[test]
fn grammar_bindings_match_the_registry_structural_rows() {
for registry in RegistryLanguage::all() {
assert_eq!(
Language::from_registry(registry).is_some(),
registry.structural().is_some(),
"{} grammar binding disagrees with its registry row",
registry.name()
);
}
for &lang in Language::all() {
assert_eq!(Language::from_registry(lang.registry()), Some(lang));
for ext in lang.registry().extensions() {
assert_eq!(Language::from_extension(ext), Some(lang), "ext {ext}");
}
}
assert_eq!(Language::from_extension("dart"), None);
assert_eq!(Language::from_name("dockerfile"), None);
}
#[test]
fn name_round_trips_for_every_language() {
for &lang in Language::all() {
assert_eq!(Language::from_name(lang.name()), Some(lang));
}
}
#[test]
fn primary_extension_resolves_back_to_the_language() {
for &lang in Language::all() {
assert_eq!(
Language::from_extension(lang.primary_extension()),
Some(lang),
"primary extension for {} does not round-trip",
lang.name()
);
}
}
#[test]
fn detect_prefers_hint_over_extension() {
let path = std::path::Path::new("foo.ts");
assert_eq!(Language::detect(path, None), Some(Language::TypeScript));
assert_eq!(
Language::detect(path, Some("javascript")),
Some(Language::JavaScript)
);
}
#[test]
fn edit_primitives_are_universal_rename_is_gated() {
for &lang in Language::all() {
let caps = lang.edit_capabilities();
assert!(caps.apply_node, "{} should support apply_node", lang.name());
assert!(
caps.insert_at_anchor,
"{} should support insert_at_anchor",
lang.name()
);
assert_eq!(
caps.rename_symbol,
lang.rename_identifier_kinds().is_some(),
"{} rename capability must match its identifier-kind table",
lang.name()
);
}
assert!(!Language::Json.edit_capabilities().rename_symbol);
assert!(!Language::Json.edit_capabilities().symbols);
assert!(Language::Rust.edit_capabilities().rename_symbol);
assert!(Language::Rust.edit_capabilities().symbols);
}
}