Skip to content

Commit

Permalink
Implement more robust runtime collection and compilation
Browse files Browse the repository at this point in the history
On top of the existing runtime this improvement also tries to check the
maven or gradle (todo) to collect as much information about the runtime
of the project, if that is not possible, the user might be prompted to
enter the home directory of the runtime, the user might also configure
the runtimes statically in the plugin's configuration.

Furthermore dependencies on other plugins such as nvim-jdtls is removed,
and the changes rely only on native the nvim lsp or coc.nvim.
  • Loading branch information
asmodeus812 committed Oct 15, 2024
1 parent 8003930 commit 84c5342
Show file tree
Hide file tree
Showing 8 changed files with 312 additions and 85 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
"rcasia/neotest-java",
ft = "java",
dependencies = {
"mfussenegger/nvim-jdtls",
"mfussenegger/nvim-dap", -- for the debugger
"rcarriga/nvim-dap-ui", -- recommended
"theHamsta/nvim-dap-virtual-text", -- recommended
Expand Down Expand Up @@ -89,7 +88,6 @@
"rcasia/neotest-java",
ft = "java",
dependencies = {
"mfussenegger/nvim-jdtls",
"mfussenegger/nvim-dap", -- for the debugger
"rcarriga/nvim-dap-ui", -- recommended
"theHamsta/nvim-dap-virtual-text", -- recommended
Expand Down Expand Up @@ -127,11 +125,23 @@

```lua
{
junit_jar = nil, -- default: stdpath("data") .. /nvim/neotest-java/junit-platform-console-standalone-[version].jar
incremental_build = true
junit_jar = nil, -- default: stdpath("data") .. /nvim/neotest-java/junit-platform-console-standalone-[version].jar
incremental_build = true
java_runtimes = {
-- there are no runtimes defined by default, if you wish to have neotest-java resolve them based on your environment define them here, one could also define environment variables with the same key/names i.e. `JAVA_HOME_8` or `JAVA_HOME_11` or `JAVA_HOME_17` etc in your zshenv or equivalent.
["JAVA_HOME_8"] = "/absolute/path/to/jdk8/home/directory",
["JAVA_HOME_11"] = "/absolute/path/to/jdk11/home/directory",
["JAVA_HOME_17"] = "/absolute/path/to/jdk17/home/directory",
},
}

```

`Note that neotest-java would try it's best to determine the current project's runtime based on the currently running lsp servers,
neotest-java supports both native neovim lsp and coc.nvim, it would try to fallback to your project configuration, supports both maven
(reading from pom.xml) & gradle (reading from build.gradle or gradle.properties). In case the runtime is found but the location of it is not
defined, neotest-java would prompt the user to input the absolute directory for the specific runtime version (only once).`

## :octocat: Contributing

Feel free to contribute to this project by creating issues for bug
Expand Down
4 changes: 2 additions & 2 deletions lua/neotest-java/command/binaries.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ local binaries = {
return compatible_path(jdtls_java_home .. "/bin/java")
end

logger.warn("JAVA_HOME setting not found in jdtls. Using defualt binary: java")
logger.warn("Unable to detect JAVA_HOME. Using defualt binary in path: java")
return "java"
end,

Expand All @@ -22,7 +22,7 @@ local binaries = {
return compatible_path(jdtls_java_home .. "/bin/javac")
end

logger.warn("JAVA_HOME setting not found in jdtls. Using default: javac")
logger.warn("Unable to detect JAVA_HOME. Using defualt binary in path: java")
return "javac"
end,
}
Expand Down
36 changes: 36 additions & 0 deletions lua/neotest-java/command/classpath.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
local lsp = require("neotest-java.lsp")
local log = require("neotest-java.logger")

---@param additional_classpath_entries string[]
---@return string[]
local function get_classpaths(additional_classpath_entries)
additional_classpath_entries = additional_classpath_entries or {}

-- this is not good ????
local bufnr = vim.api.nvim_get_current_buf()
local uri = vim.uri_from_bufnr(bufnr)
local result_classpaths = {}

for _, v in ipairs(additional_classpath_entries) do
table.insert(result_classpaths, v)
end

for _, scope in ipairs({ "runtime", "test" }) do
local options = vim.json.encode({ scope = scope })
local err, result, client = lsp.execute_command("workspace/executeCommand", {
command = "java.project.getClasspaths",
arguments = { uri, options },
}, bufnr)
if result == nil or err ~= nil or client == nil then
log.warn(string.format("Unable to resolve [%s] classpahts", scope))
else
for _, v in ipairs(result.classpaths or {}) do
table.insert(result_classpaths, v)
end
end
end

return result_classpaths
end

return get_classpaths
12 changes: 12 additions & 0 deletions lua/neotest-java/command/compile.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
local log = require("neotest-java.logger")
local lsp = require("neotest-java.lsp")
local nio = require("nio")

---@param compilation_type string
local function run_compile(compilation_type)
local bufnr = nio.api.nvim_get_current_buf()
lsp.execute_command("java/buildWorkspace", compilation_type == "full", bufnr)
log.info("Triggered build for the current workspace")
end

return run_compile
64 changes: 5 additions & 59 deletions lua/neotest-java/command/jdtls.lua
Original file line number Diff line number Diff line change
@@ -1,68 +1,14 @@
local nio = require("nio")
local runtime = require("neotest-java.command.runtime")
local classpaths = require("neotest-java.command.classpath")

local write_file = require("neotest-java.util.write_file")
local compatible_path = require("neotest-java.util.compatible_path")

local M = {}

M.get_java_home = function()
local bufnr = vim.api.nvim_get_current_buf()
local uri = vim.uri_from_bufnr(bufnr)
local future = nio.control.future()

local setting = "org.eclipse.jdt.ls.core.vm.location"
local cmd = {
command = "java.project.getSettings",
arguments = { uri, { setting } },
}
require("jdtls.util").execute_command(cmd, function(err1, resp)
assert(not err1, vim.inspect(err1))
future.set(resp)
end, bufnr)

local java_exec = future.wait()

return java_exec[setting]
end

---@param additional_classpath_entries string[]
M.get_classpath = function(additional_classpath_entries)
additional_classpath_entries = additional_classpath_entries or {}
M.get_java_home = runtime

local classpaths = {}

local bufnr = vim.api.nvim_get_current_buf()
local uri = vim.uri_from_bufnr(bufnr)
local runtime_classpath_future = nio.control.future()
local test_classpath_future = nio.control.future()

---@param future nio.control.Future
for scope, future in pairs({ ["runtime"] = runtime_classpath_future, ["test"] = test_classpath_future }) do
local options = vim.json.encode({ scope = scope })
local cmd = {
command = "java.project.getClasspaths",
arguments = { uri, options },
}
require("jdtls.util").execute_command(cmd, function(err1, resp)
assert(not err1, vim.inspect(err1))

future.set(resp.classpaths)
end, bufnr)
end
local runtime_classpaths = runtime_classpath_future.wait()
local test_classpaths = test_classpath_future.wait()

for _, v in ipairs(additional_classpath_entries) do
classpaths[#classpaths + 1] = v
end
for _, v in ipairs(runtime_classpaths) do
classpaths[#classpaths + 1] = v
end
for _, v in ipairs(test_classpaths) do
classpaths[#classpaths + 1] = v
end

return classpaths
end
M.get_classpath = classpaths

M.get_classpath_file_argument = function(report_dir, additional_classpath_entries)
local classpath = table.concat(M.get_classpath(additional_classpath_entries), ":")
Expand Down
163 changes: 163 additions & 0 deletions lua/neotest-java/command/runtime.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
local File = require("neotest.lib.file")

local read_xml_tag = require("neotest-java.util.read_xml_tag")
local ch = require("neotest-java.context_holder")
local log = require("neotest-java.logger")
local lsp = require("neotest-java.lsp")
local nio = require("nio")

local COMPILER = "org.eclipse.jdt.core.compiler.source"
local LOCATION = "org.eclipse.jdt.ls.core.vm.location"
local RUNTIMES = {}

local function get_env(var)
return nio.fn.getenv(var)
end

local function get_java_home()
return get_env("JAVA_HOME")
end

local function has_env(var)
return nio.fn.getenv(var) ~= vim.NIL
end

local function input_runtime(actual_version)
local message =
string.format("Enter runtime directory for JDK-%s (defaults to JAVA_HOME if empty): ", actual_version)
local runtime_path = nio.fn.input({
default = "",
prompt = message,
completion = "dir",
cancelreturn = "__INPUT_CANCELLED__",
})
if runtime_path == "__INPUT_CANCELLED__" then
log.info(string.format("Defaulting to JAVA_HOME due to empty user input for %s", actual_version))
return get_java_home()
elseif
not runtime_path
or #runtime_path == 0
or nio.fn.isdirectory(runtime_path) == 0
or nio.fn.isdirectory(string.format("%s/bin", runtime_path)) == 0
then
log.warn(string.format("Invalid runtime home directory %s was specified, please try again", runtime_path))
return input_runtime(actual_version)
else
log.info(string.format("Using user input %s for runtime version %s", runtime_path, actual_version))
return runtime_path
end
end

local function maven_runtime()
local context = ch.get_context()
local plugins = read_xml_tag("pom.xml", "project.build.plugins.plugin")

for _, plugin in ipairs(plugins or {}) do
if plugin.artifactId == "maven-compiler-plugin" and plugin.configuration then
if plugin.configuration.target ~= plugin.configuration.source then
error("Target and source mismatch detected in maven-compiler-plugin")
end
local target_version = vim.split(plugin.configuration.target, "%.")
local actual_version = target_version[#target_version]
if RUNTIMES[actual_version] then
return RUNTIMES[actual_version]
end

local runtime_name = string.format("JAVA_HOME_%d", actual_version)
if context and context.config.java_runtimes[runtime_name] then
return ch.get_context().config.java_runtimes[runtime_name]
elseif has_env(runtime_name) then
return get_env(runtime_name)
else
local runtime_path = input_runtime(actual_version)
RUNTIMES[actual_version] = runtime_path
return runtime_path
end
end
end
log.warn("Unable to resolve the runtime from maven-compiler-plugin, defaulting to JAVA_HOME")
return get_java_home()
end

local function gradle_runtime()
-- fix: the build.gradle has to be read to obtain information about the project's configured runtime
log.warn("Unable to resolve the runtime from build.gradle, defaulting to JAVA_HOME")
return get_java_home()
end

local function extract_runtime(bufnr)
local uri = vim.uri_from_bufnr(bufnr)
local err, settings, client = lsp.execute_command("workspace/executeCommand", {
command = "java.project.getSettings",
arguments = { uri, { COMPILER, LOCATION } },
}, bufnr)

if err ~= nil or client == nil or settings == nil then
log.info("Unable to extract runtime from an active lsp client")
return
end

local config = client.config.settings.java or {}
config = config.configuration or {}

-- location starts off being nil, we require strict matching, otherwise the
-- runtime resolve will fallback to maven or gradle, we do not want to
-- resolve to JAVA_HOME immediately here, it is too early.
local location = nil
local runtimes = config.runtimes
local compiler = settings[COMPILER]

-- we can early exit with location here
if settings[LOCATION] then
location = settings[LOCATION]
else
-- go over available runtimes and resolve it
for _, runtime in ipairs(runtimes or {}) do
-- default runtimes get priority
if runtime.default == true then
location = runtime.path
break
end
-- match runtime against compliance version
local match = runtime.name:match(".*-(.*)")
if match and match == compiler then
location = runtime.path
break
end
end
end

-- location has to be strictly resolved from the project `settings` or from
-- the runtimes in client's settings, otherwise we return `nil` for runtime
if not location or #location == 0 or nio.fn.isdirectory(location) == 0 then
return nil
end
return location
end

---@return string | nil
local function get_runtime()
local bufnr = nio.api.nvim_get_current_buf()
local runtime = extract_runtime(bufnr)

-- in case the runtime was not found, try to fetch one from the build
-- system which the current project is using, match against maven or gradle
-- and try to find the configured runtime, or fallback to JAVA_HOME
if not runtime or #runtime == 0 then
if File.exists("pom.xml") then
runtime = maven_runtime()
elseif File.exists("build.gradle") then
runtime = gradle_runtime()
end
end

if runtime and #runtime > 0 then
log.info(string.format("Resolved project runtime %s", runtime))
return runtime
else
log.warn("Unable to resolve the project's runtime")
return nil
end
end

return get_runtime
Loading

0 comments on commit 84c5342

Please sign in to comment.