diff --git a/README.md b/README.md index 495a69e..7613532 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 diff --git a/lua/neotest-java/command/binaries.lua b/lua/neotest-java/command/binaries.lua index a6f6e1e..2938309 100644 --- a/lua/neotest-java/command/binaries.lua +++ b/lua/neotest-java/command/binaries.lua @@ -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, @@ -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, } diff --git a/lua/neotest-java/command/classpath.lua b/lua/neotest-java/command/classpath.lua new file mode 100644 index 0000000..b68aec6 --- /dev/null +++ b/lua/neotest-java/command/classpath.lua @@ -0,0 +1,34 @@ +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 {} + 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 diff --git a/lua/neotest-java/command/compile.lua b/lua/neotest-java/command/compile.lua new file mode 100644 index 0000000..a3c2c47 --- /dev/null +++ b/lua/neotest-java/command/compile.lua @@ -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 diff --git a/lua/neotest-java/command/jdtls.lua b/lua/neotest-java/command/jdtls.lua index 5ed34fd..77bd87f 100644 --- a/lua/neotest-java/command/jdtls.lua +++ b/lua/neotest-java/command/jdtls.lua @@ -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), ":") diff --git a/lua/neotest-java/command/runtime.lua b/lua/neotest-java/command/runtime.lua new file mode 100644 index 0000000..d5d7906 --- /dev/null +++ b/lua/neotest-java/command/runtime.lua @@ -0,0 +1,165 @@ +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 + 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, 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 diff --git a/lua/neotest-java/context_holder.lua b/lua/neotest-java/context_holder.lua index aef6b1c..c028d56 100644 --- a/lua/neotest-java/context_holder.lua +++ b/lua/neotest-java/context_holder.lua @@ -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 @@ -31,6 +32,7 @@ return { ---@class neotest-java.ConfigOpts ---@field junit_jar string ---@field incremental_build boolean +---@field java_runtimes table ---@class neotest-java.Context ---@field config neotest-java.ConfigOpts diff --git a/lua/neotest-java/core/spec_builder.lua b/lua/neotest-java/core/spec_builder.lua index d8a62ef..41ccc4e 100644 --- a/lua/neotest-java/core/spec_builder.lua +++ b/lua/neotest-java/core/spec_builder.lua @@ -1,14 +1,17 @@ local CommandBuilder = require("neotest-java.command.junit_command_builder") -local resolve_qualfied_name = require("neotest-java.util.resolve_qualified_name") -local logger = require("neotest-java.logger") -local random_port = require("neotest-java.util.random_port") -local build_tools = require("neotest-java.build_tool") -local nio = require("nio") -local path = require("plenary.path") -local compatible_path = require("neotest-java.util.compatible_path") local Project = require("neotest-java.types.project") +local build_tools = require("neotest-java.build_tool") local ch = require("neotest-java.context_holder") +local compatible_path = require("neotest-java.util.compatible_path") +local compile = require("neotest-java.command.compile") local find_module_by_filepath = require("neotest-java.util.find_module_by_filepath") +local logger = require("neotest-java.logger") +local random_port = require("neotest-java.util.random_port") +local resolve_qualfied_name = require("neotest-java.util.resolve_qualified_name") + +local path = require("plenary.path") +local lib = require("neotest.lib") +local nio = require("nio") local SpecBuilder = {} @@ -17,20 +20,11 @@ local SpecBuilder = {} ---@param config neotest-java.ConfigOpts ---@return nil | neotest.RunSpec | neotest.RunSpec[] function SpecBuilder.build_spec(args, project_type, config) - -- check that required dependencies are present - local ok_jdtls, jdtls = pcall(require, "jdtls") - assert(ok_jdtls, "neotest-java requires nvim-jdtls to tests") - if args.strategy == "dap" then local ok_dap, _ = pcall(require, "dap") assert(ok_dap, "neotest-java requires nvim-dap to run debug tests") end - -- check there is an active java client - local has_jdtls_client = #nio.lsp.get_clients({ name = "jdtls" }) ~= 0 - - assert(has_jdtls_client, "there is no jdtls client attached.") - local command = CommandBuilder:new(config, project_type) local tree = args.tree local position = tree:data() @@ -85,10 +79,8 @@ function SpecBuilder.build_spec(args, project_type, config) -- COMPILATION STEP local compile_mode = ch.config().incremental_build and "incremental" or "full" logger.debug(("compilation in %s mode"):format(compile_mode)) - nio.run(function(_) - nio.scheduler() - jdtls.compile(compile_mode) - end):wait() + lib.notify("Compiling source & test files...") + compile(compile_mode) logger.debug("compilation complete!") -- DAP STRATEGY diff --git a/lua/neotest-java/lsp/init.lua b/lua/neotest-java/lsp/init.lua new file mode 100644 index 0000000..0a120f4 --- /dev/null +++ b/lua/neotest-java/lsp/init.lua @@ -0,0 +1,68 @@ +local log = require("neotest-java.logger") +local nio = require("nio") + +-- table that holds the language server settings, this is mostly done for interfacing with coc.nvim, since native neovim lsp clients hold their settings in the clients table +local SETTINGS = {} + +local function coc_command(id, params, _) + SETTINGS = nio.fn["coc#util#get_config"]("java") + if params == nil then + params = {} + end + if type(params) ~= "table" then + params = { params } + end + local ok_services, services = pcall(nio.fn.CocAction, "services") + services = ok_services and vim.tbl_filter(function(service) + return service and service.state == "running" and service.id == "java" + end, services) or {} + assert(#services > 0, "there is no jdtls client attached") + + local ok_request, result = pcall(nio.fn.CocRequest, "java", id, params) + if not ok_request or not result then + log.warn( + string.format( + "Unable to run lsp request %s with payload %s", id, vim.inspect(params) + ) + ) + end + local err = not ok_request and result ~= vim.NIL and { message = result } or nil + return err, + result, + { + -- adapter for the native lsp client talbe format, to simplify external clients using this interface to talk to the lsp client, which ever it happens to be + name = "jdtls", + config = { + settings = { + java = SETTINGS, + }, + }, + } +end + +local function lsp_command(id, params, bufnr) + local clients = vim.lsp.get_clients({ name = "jdtls" }) + assert(#clients > 0, "there is no jdtls client attached") + + local response, error = clients[1].request_sync(id, params, 5000, bufnr) + if error then + log.warn( + string.format( + "Unable to run lsp command %s with payload %s", id, vim.inspect(params) + ) + ) + end + return error, response.result, clients[1] +end + +local function execute_command(id, params, bufnr) + if vim.g.did_coc_loaded ~= nil then + return coc_command(id, params, bufnr) + else + return lsp_command(id, params, bufnr) + end +end + +return { + execute_command = execute_command, +}