diff --git a/crates/cli/src/init.rs b/crates/cli/src/init.rs index 9eacdb25..b0cdb586 100644 --- a/crates/cli/src/init.rs +++ b/crates/cli/src/init.rs @@ -36,6 +36,8 @@ const PARSER_CLASS_NAME_PLACEHOLDER: &str = "PARSER_CLASS_NAME"; const PARSER_DESCRIPTION_PLACEHOLDER: &str = "PARSER_DESCRIPTION"; const PARSER_LICENSE_PLACEHOLDER: &str = "PARSER_LICENSE"; +const PARSER_NS_PLACEHOLDER: &str = "PARSER_NS"; +const PARSER_NS_CLEANED_PLACEHOLDER: &str = "PARSER_NS_CLEANED"; const PARSER_URL_PLACEHOLDER: &str = "PARSER_URL"; const PARSER_URL_STRIPPED_PLACEHOLDER: &str = "PARSER_URL_STRIPPED"; const PARSER_VERSION_PLACEHOLDER: &str = "PARSER_VERSION"; @@ -58,6 +60,11 @@ const AUTHOR_BLOCK_RS: &str = "\nauthors = ["; const AUTHOR_NAME_PLACEHOLDER_RS: &str = "PARSER_AUTHOR_NAME"; const AUTHOR_EMAIL_PLACEHOLDER_RS: &str = " PARSER_AUTHOR_EMAIL"; +const AUTHOR_BLOCK_JAVA: &str = "\n "; +const AUTHOR_NAME_PLACEHOLDER_JAVA: &str = "\n PARSER_AUTHOR_NAME"; +const AUTHOR_EMAIL_PLACEHOLDER_JAVA: &str = "\n PARSER_AUTHOR_EMAIL"; +const AUTHOR_URL_PLACEHOLDER_JAVA: &str = "\n PARSER_AUTHOR_URL"; + const AUTHOR_BLOCK_GRAMMAR: &str = "\n * @author "; const AUTHOR_NAME_PLACEHOLDER_GRAMMAR: &str = "PARSER_AUTHOR_NAME"; const AUTHOR_EMAIL_PLACEHOLDER_GRAMMAR: &str = " PARSER_AUTHOR_EMAIL"; @@ -107,6 +114,10 @@ const TEST_BINDING_PY_TEMPLATE: &str = include_str!("./templates/test_binding.py const PACKAGE_SWIFT_TEMPLATE: &str = include_str!("./templates/package.swift"); const TESTS_SWIFT_TEMPLATE: &str = include_str!("./templates/tests.swift"); +const POM_XML_TEMPLATE: &str = include_str!("./templates/pom.xml"); +const BINDING_JAVA_TEMPLATE: &str = include_str!("./templates/binding.java"); +const TEST_JAVA_TEMPLATE: &str = include_str!("./templates/test.java"); + const BUILD_ZIG_TEMPLATE: &str = include_str!("./templates/build.zig"); const BUILD_ZIG_ZON_TEMPLATE: &str = include_str!("./templates/build.zig.zon"); const ROOT_ZIG_TEMPLATE: &str = include_str!("./templates/root.zig"); @@ -134,6 +145,7 @@ pub struct JsonConfigOpts { pub email: Option, #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, + pub namespace: Option, pub bindings: Bindings, } @@ -174,7 +186,7 @@ impl JsonConfigOpts { }), funding: self.funding, }), - namespace: None, + namespace: self.namespace, }, bindings: self.bindings, } @@ -197,6 +209,7 @@ impl Default for JsonConfigOpts { author: String::new(), email: None, url: None, + namespace: None, bindings: Bindings::default(), } } @@ -218,6 +231,7 @@ struct GenerateOpts<'a> { injections_query_path: &'a str, locals_query_path: &'a str, tags_query_path: &'a str, + namespace: Option<&'a str>, } pub fn generate_grammar_files( @@ -311,6 +325,7 @@ pub fn generate_grammar_files( tags_query_path: tree_sitter_config.grammars[0] .tags .to_variable_value(&default_tags_path), + namespace: tree_sitter_config.metadata.namespace.as_deref(), }; // Create package.json @@ -1048,6 +1063,45 @@ pub fn generate_grammar_files( })?; } + // Generate Java bindings + if tree_sitter_config.bindings.java { + missing_path(repo_path.join("pom.xml"), |path| { + generate_file(path, POM_XML_TEMPLATE, language_name, &generate_opts) + })?; + + missing_path(bindings_dir.join("java"), create_dir)?.apply(|path| { + missing_path(path.join("main"), create_dir)?.apply(|path| { + let package_path = generate_opts + .namespace + .unwrap_or("io.github.treesitter") + .replace(['-', '_'], "") + .split('.') + .fold(path.to_path_buf(), |path, dir| path.join(dir)) + .join("jtreesitter") + .join(language_name.to_lowercase().replace('_', "")); + missing_path(package_path, create_dir)?.apply(|path| { + missing_path(path.join(format!("{class_name}.java")), |path| { + generate_file(path, BINDING_JAVA_TEMPLATE, language_name, &generate_opts) + })?; + + Ok(()) + })?; + + Ok(()) + })?; + + missing_path(path.join("test"), create_dir)?.apply(|path| { + missing_path(path.join(format!("{class_name}Test.java")), |path| { + generate_file(path, TEST_JAVA_TEMPLATE, language_name, &generate_opts) + })?; + + Ok(()) + })?; + + Ok(()) + })?; + } + Ok(()) } @@ -1097,6 +1151,15 @@ fn generate_file( ) -> Result<()> { let filename = path.file_name().unwrap().to_str().unwrap(); + let lower_parser_name = if path + .extension() + .is_some_and(|e| e.eq_ignore_ascii_case("java")) + { + language_name.to_snake_case().replace('_', "") + } else { + language_name.to_snake_case() + }; + let mut replacement = template .replace( CAMEL_PARSER_NAME_PLACEHOLDER, @@ -1110,14 +1173,11 @@ fn generate_file( UPPER_PARSER_NAME_PLACEHOLDER, &language_name.to_shouty_snake_case(), ) - .replace( - LOWER_PARSER_NAME_PLACEHOLDER, - &language_name.to_snake_case(), - ) .replace( KEBAB_PARSER_NAME_PLACEHOLDER, &language_name.to_kebab_case(), ) + .replace(LOWER_PARSER_NAME_PLACEHOLDER, &lower_parser_name) .replace(PARSER_NAME_PLACEHOLDER, language_name) .replace(CLI_VERSION_PLACEHOLDER, CLI_VERSION) .replace(RUST_BINDING_VERSION_PLACEHOLDER, RUST_BINDING_VERSION) @@ -1157,6 +1217,9 @@ fn generate_file( "Cargo.toml" => { replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_RS, ""); } + "pom.xml" => { + replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_JAVA, ""); + } _ => {} } } @@ -1182,30 +1245,52 @@ fn generate_file( "Cargo.toml" => { replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_RS, ""); } + "pom.xml" => { + replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_JAVA, ""); + } _ => {} } } - if filename == "package.json" { - if let Some(url) = generate_opts.author_url { + match (generate_opts.author_url, filename) { + (Some(url), "package.json" | "pom.xml") => { replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER, url); - } else { + } + (None, "package.json") => { replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER_JS, ""); } + (None, "pom.xml") => { + replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER_JAVA, ""); + } + _ => {} } if generate_opts.author_name.is_none() && generate_opts.author_email.is_none() && generate_opts.author_url.is_none() - && filename == "package.json" { - if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JS) { - if let Some(end_idx) = replacement[start_idx..] - .find("},") - .map(|i| i + start_idx + 2) - { - replacement.replace_range(start_idx..end_idx, ""); + match filename { + "package.json" => { + if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JS) { + if let Some(end_idx) = replacement[start_idx..] + .find("},") + .map(|i| i + start_idx + 2) + { + replacement.replace_range(start_idx..end_idx, ""); + } + } } + "pom.xml" => { + if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JAVA) { + if let Some(end_idx) = replacement[start_idx..] + .find("") + .map(|i| i + start_idx + 12) + { + replacement.replace_range(start_idx..end_idx, ""); + } + } + } + _ => {} } } else if generate_opts.author_name.is_none() && generate_opts.author_email.is_none() { match filename { @@ -1286,6 +1371,19 @@ fn generate_file( ); } + if let Some(namespace) = generate_opts.namespace { + replacement = replacement + .replace( + PARSER_NS_CLEANED_PLACEHOLDER, + &namespace.replace(['-', '_'], ""), + ) + .replace(PARSER_NS_PLACEHOLDER, namespace); + } else { + replacement = replacement + .replace(PARSER_NS_CLEANED_PLACEHOLDER, "io.github.treesitter") + .replace(PARSER_NS_PLACEHOLDER, "io.github.tree-sitter"); + } + if let Some(funding_url) = generate_opts.funding { match filename { "pyproject.toml" | "package.json" => { diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 81a67991..937e54b4 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -772,6 +772,14 @@ impl Init { .map(|e| Some(e.trim().to_string())) }; + let namespace = || { + Input::::with_theme(&ColorfulTheme::default()) + .with_prompt("Package namespace") + .default("io.github.tree-sitter".to_string()) + .allow_empty(true) + .interact() + }; + let bindings = || { let languages = Bindings::default().languages(); @@ -801,6 +809,7 @@ impl Init { "author", "email", "url", + "namespace", "bindings", "exit", ]; @@ -821,6 +830,7 @@ impl Init { "author" => opts.author = author()?, "email" => opts.email = email()?, "url" => opts.url = url()?, + "namespace" => opts.namespace = Some(namespace()?), "bindings" => opts.bindings = bindings()?, "exit" => break, _ => unreachable!(), diff --git a/crates/cli/src/templates/.editorconfig b/crates/cli/src/templates/.editorconfig index c4650c59..ff17b12f 100644 --- a/crates/cli/src/templates/.editorconfig +++ b/crates/cli/src/templates/.editorconfig @@ -3,7 +3,7 @@ root = true [*] charset = utf-8 -[*.{json,toml,yml,gyp}] +[*.{json,toml,yml,gyp,xml}] indent_style = space indent_size = 2 @@ -31,6 +31,10 @@ indent_size = 4 indent_style = space indent_size = 4 +[*.java] +indent_style = space +indent_size = 4 + [*.go] indent_style = tab indent_size = 8 diff --git a/crates/cli/src/templates/binding.java b/crates/cli/src/templates/binding.java new file mode 100644 index 00000000..704064a0 --- /dev/null +++ b/crates/cli/src/templates/binding.java @@ -0,0 +1,65 @@ +package PARSER_NS_CLEANED.jtreesitter.LOWER_PARSER_NAME; + +import java.lang.foreign.*; + +public final class PARSER_CLASS_NAME { + private static final ValueLayout VOID_PTR = + ValueLayout.ADDRESS.withTargetLayout(MemoryLayout.sequenceLayout(Long.MAX_VALUE, ValueLayout.JAVA_BYTE)); + private static final FunctionDescriptor FUNC_DESC = FunctionDescriptor.of(VOID_PTR); + private static final Linker LINKER = Linker.nativeLinker(); + private static final PARSER_CLASS_NAME INSTANCE = new PARSER_CLASS_NAME(); + + private final Arena arena = Arena.ofAuto(); + private volatile SymbolLookup lookup = null; + + private PARSER_CLASS_NAME() {} + + /** + * Get the tree-sitter language for this grammar. + */ + public static MemorySegment language() { + if (INSTANCE.lookup == null) + INSTANCE.lookup = INSTANCE.findLibrary(); + return language(INSTANCE.lookup); + } + + /** + * Get the tree-sitter language for this grammar. + * + * The {@linkplain Arena} used in the {@code lookup} + * must not be closed while the language is being used. + */ + public static MemorySegment language(SymbolLookup lookup) { + return call(lookup, "tree_sitter_PARSER_NAME"); + } + + private SymbolLookup findLibrary() { + try { + var library = System.mapLibraryName("tree-sitter-KEBAB_PARSER_NAME"); + return SymbolLookup.libraryLookup(library, arena); + } catch (IllegalArgumentException ex1) { + try { + System.loadLibrary("tree-sitter-KEBAB_PARSER_NAME"); + return SymbolLookup.loaderLookup(); + } catch (UnsatisfiedLinkError ex2) { + ex1.addSuppressed(ex2); + throw ex1; + } + } + } + + private static UnsatisfiedLinkError unresolved(String name) { + return new UnsatisfiedLinkError("Unresolved symbol: %s".formatted(name)); + } + + @SuppressWarnings("SameParameterValue") + private static MemorySegment call(SymbolLookup lookup, String name) throws UnsatisfiedLinkError { + var address = lookup.find(name).orElseThrow(() -> unresolved(name)); + try { + var function = LINKER.downcallHandle(address, FUNC_DESC); + return (MemorySegment) function.invokeExact(); + } catch (Throwable e) { + throw new RuntimeException("Call to %s failed".formatted(name), e); + } + } +} diff --git a/crates/cli/src/templates/gitattributes b/crates/cli/src/templates/gitattributes index 7772c942..027ac707 100644 --- a/crates/cli/src/templates/gitattributes +++ b/crates/cli/src/templates/gitattributes @@ -40,3 +40,7 @@ Package.resolved linguist-generated bindings/zig/* linguist-generated build.zig linguist-generated build.zig.zon linguist-generated + +# Java bindings +pom.xml linguist-generated +bindings/java/** linguist-generated diff --git a/crates/cli/src/templates/gitignore b/crates/cli/src/templates/gitignore index bc9e191a..7c0cb7f5 100644 --- a/crates/cli/src/templates/gitignore +++ b/crates/cli/src/templates/gitignore @@ -45,3 +45,4 @@ zig-out/ *.tar.gz *.tgz *.zip +*.jar diff --git a/crates/cli/src/templates/pom.xml b/crates/cli/src/templates/pom.xml new file mode 100644 index 00000000..661fe42b --- /dev/null +++ b/crates/cli/src/templates/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + PARSER_NS + jtreesitter-KEBAB_PARSER_NAME + JTreeSitter CAMEL_PARSER_NAME + PARSER_VERSION + PARSER_DESCRIPTION + PARSER_URL + + + PARSER_LICENSE + https://spdx.org/licenses/PARSER_LICENSE.html + + + + + PARSER_AUTHOR_NAME + PARSER_AUTHOR_EMAIL + PARSER_AUTHOR_URL + + + + PARSER_URL + scm:git:git://PARSER_URL_STRIPPED.git + scm:git:ssh://PARSER_URL_STRIPPED.git + + + 23 + UTF-8 + true + true + false + true + + + + io.github.tree-sitter + jtreesitter + 0.26.0 + true + + + org.junit.jupiter + junit-jupiter-api + 6.0.1 + test + + + + bindings/java/main + bindings/java/test + + + maven-surefire-plugin + 3.5.4 + + + ${project.build.directory}/reports/surefire + + --enable-native-access=ALL-UNNAMED + + + + maven-javadoc-plugin + 3.12.0 + + + + jar + + + + + public + true + true + all,-missing + + + + maven-source-plugin + 3.3.1 + + + + jar-no-fork + + + + + + maven-gpg-plugin + 3.2.8 + + + verify + + sign + + + true + + --no-tty + --pinentry-mode + loopback + + + + + + + io.github.mavenplugins + central-publishing-maven-plugin + 1.1.1 + + + deploy + + publish + + + validated + ${publish.auto} + ${publish.skip} + ${project.artifactId}-${project.version}.zip + ${project.artifactId}-${project.version}.zip + + + + true + + + + + + ci + + + env.CI + true + + + + false + true + false + + + + diff --git a/crates/cli/src/templates/test.java b/crates/cli/src/templates/test.java new file mode 100644 index 00000000..8bf81ea0 --- /dev/null +++ b/crates/cli/src/templates/test.java @@ -0,0 +1,12 @@ +import io.github.treesitter.jtreesitter.Language; +import PARSER_NS_CLEANED.jtreesitter.LOWER_PARSER_NAME.PARSER_CLASS_NAME; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class PARSER_CLASS_NAMETest { + @Test + public void testCanLoadLanguage() { + assertDoesNotThrow(() -> new Language(PARSER_CLASS_NAME.language())); + } +} diff --git a/crates/loader/src/loader.rs b/crates/loader/src/loader.rs index 16819cc4..d64b89de 100644 --- a/crates/loader/src/loader.rs +++ b/crates/loader/src/loader.rs @@ -450,7 +450,6 @@ pub struct Links { pub struct Bindings { pub c: bool, pub go: bool, - #[serde(skip)] pub java: bool, #[serde(skip)] pub kotlin: bool, @@ -464,12 +463,12 @@ pub struct Bindings { impl Bindings { /// return available languages and its default enabled state. #[must_use] - pub const fn languages(&self) -> [(&'static str, bool); 7] { + pub const fn languages(&self) -> [(&'static str, bool); 8] { [ ("c", true), ("go", true), - // Comment out Java and Kotlin until the bindings are actually available. - // ("java", false), + ("java", false), + // Comment out Kotlin until the bindings are actually available. // ("kotlin", false), ("node", true), ("python", true), @@ -500,8 +499,8 @@ impl Bindings { match v { "c" => out.c = true, "go" => out.go = true, - // Comment out Java and Kotlin until the bindings are actually available. - // "java" => out.java = true, + "java" => out.java = true, + // Comment out Kotlin until the bindings are actually available. // "kotlin" => out.kotlin = true, "node" => out.node = true, "python" => out.python = true,