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 16, 2024
1 parent 8003930 commit 5b19821
Show file tree
Hide file tree
Showing 9 changed files with 324 additions and 86 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
6 changes: 3 additions & 3 deletions lua/neotest-java/command/binaries.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
local logger = require("neotest-java.logger")
local jdtls = require("neotest-java.command.jdtls")
local compatible_path = require("neotest-java.util.compatible_path")
local logger = require("neotest-java.logger")

local binaries = {

Expand All @@ -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: javac")
return "javac"
end,
}
Expand Down
35 changes: 35 additions & 0 deletions lua/neotest-java/command/classpath.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
local log = require("neotest-java.logger")
local lsp = require("neotest-java.lsp")
local nio = require("nio")

---@param additional_classpath_entries string[]
---@return string[]
local function get_classpaths(additional_classpath_entries)
additional_classpath_entries = additional_classpath_entries or {}
local bufnr = nio.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 = lsp.execute_command("workspace/executeCommand", {
command = "java.project.getClasspaths",
arguments = { uri, options },
}, bufnr)
if result == nil or err ~= nil then
log.warn(string.format("Unable to resolve [%s] target 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
18 changes: 18 additions & 0 deletions lua/neotest-java/command/compile.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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()
local err, result = lsp.execute_command("java/buildWorkspace", compilation_type == "full", bufnr)
if result == nil or err ~= nil then
log.warn(string.format("Unable to build with [%s] mode", compilation_type))
else
log.info(string.format("Built workspace with mode %s", compilation_type))
end

return result
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
167 changes: 167 additions & 0 deletions lua/neotest-java/command/runtime.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
local File = require("neotest.lib.file")

local read_xml_tag = require("neotest-java.util.read_xml_tag")
local context_holder = 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 has_env(var)
return nio.fn.getenv(var) ~= vim.NIL
end

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

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

local function input_runtime(actual_version)
local message =
string.format("Enter runtime home directory for JDK-%s (default 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 = context_holder.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
assert(plugin.configuration.target == plugin.configuration.source,
"Target and source mismatch detected in maven-compiler-plugin")

local target_version = vim.split(plugin.configuration.target, "%.")
local actual_version = #target_version > 0 and 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 context.config.java_runtimes[runtime_name]
elseif has_env(runtime_name) then
return get_env(runtime_name)
elseif actual_version ~= nil then
local runtime_path = input_runtime(actual_version)
RUNTIMES[actual_version] = runtime_path
return runtime_path
else
log.warn("Detected maven-compiler-plugin, but unable to resolve runtime version")
break
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, result, settings = lsp.execute_command("workspace/executeCommand", {
command = "java.project.getSettings",
arguments = { uri, { COMPILER, LOCATION } },
}, bufnr)

if err ~= nil or result == nil or settings == nil then
log.warn("Unable to extract runtime from lsp client")
return
end
-- used to obtain the current language server or workspace folder configuration for the the java runtimes and their locations
local config = settings.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 = result[COMPILER]

-- we can early exit with location here, when the location exists, however that might not always be, therefore if no location is
-- present we try to resolve which is the default runtime configured for the project based on the language server or workspace,
-- that is a last resort option, but still provides a valid way to resolve the runtime if all else fails
-- settings
if result[LOCATION] then
location = result[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, and has to point
-- to a valid directory on the filesystem, 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
2 changes: 2 additions & 0 deletions lua/neotest-java/context_holder.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ local compatible_path = require("neotest-java.util.compatible_path")
local default_config = {
junit_jar = compatible_path(vim.fn.stdpath("data") .. "/neotest-java/junit-platform-console-standalone-1.10.1.jar"),
incremental_build = true,
java_runtimes = {}
}

---@type neotest-java.Context
Expand All @@ -31,6 +32,7 @@ return {
---@class neotest-java.ConfigOpts
---@field junit_jar string
---@field incremental_build boolean
---@field java_runtimes table<string,string>

---@class neotest-java.Context
---@field config neotest-java.ConfigOpts
Expand Down
Loading

0 comments on commit 5b19821

Please sign in to comment.