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,