From 0c5614439d542299a150b403fdfe4f1462a58d77 Mon Sep 17 00:00:00 2001 From: Svetlozar Iliev Date: Tue, 15 Oct 2024 17:20:11 +0300 Subject: [PATCH] Implement more robust runtime collection and compilation 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. --- README.md | 18 ++- lua/neotest-java/command/binaries.lua | 6 +- lua/neotest-java/command/classpath.lua | 35 ++++++ lua/neotest-java/command/compile.lua | 18 +++ lua/neotest-java/command/jdtls.lua | 64 +--------- lua/neotest-java/command/runtime.lua | 166 +++++++++++++++++++++++++ lua/neotest-java/context_holder.lua | 2 + lua/neotest-java/core/spec_builder.lua | 42 ++++--- lua/neotest-java/lsp/init.lua | 66 ++++++++++ 9 files changed, 331 insertions(+), 86 deletions(-) create mode 100644 lua/neotest-java/command/classpath.lua create mode 100644 lua/neotest-java/command/compile.lua create mode 100644 lua/neotest-java/command/runtime.lua create mode 100644 lua/neotest-java/lsp/init.lua 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..d9befca 100644 --- a/lua/neotest-java/command/binaries.lua +++ b/lua/neotest-java/command/binaries.lua @@ -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 = { @@ -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: javac") 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..dcfdeab --- /dev/null +++ b/lua/neotest-java/command/classpath.lua @@ -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, settings = lsp.execute_command("workspace/executeCommand", { + command = "java.project.getClasspaths", + arguments = { uri, options }, + }, bufnr) + if result == nil or err ~= nil or settings == 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 diff --git a/lua/neotest-java/command/compile.lua b/lua/neotest-java/command/compile.lua new file mode 100644 index 0000000..26c2a68 --- /dev/null +++ b/lua/neotest-java/command/compile.lua @@ -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 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..b26c92d --- /dev/null +++ b/lua/neotest-java/command/runtime.lua @@ -0,0 +1,166 @@ +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.info("Unable to extract runtime from an active lsp client") + return + end + + local config = settings.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 = result[COMPILER] + + -- we can early exit with location here + 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, 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..d2fddce 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,18 @@ 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...") + local result = compile(compile_mode) + if result == 0 then + lib.notify("Compiling project files has failed", vim.log.levels.WARN) + elseif result == 1 then + lib.notify("Compiled project files successfully", vim.log.levels.INFO) + elseif result == 2 then + lib.notify("Compiled project files with errors", vim.log.levels.WARN) + else + lib.notify("Compilation of project files has been canceled", vim.log.levels.INFO) + end 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..c7c6b2f --- /dev/null +++ b/lua/neotest-java/lsp/init.lua @@ -0,0 +1,66 @@ +local log = require("neotest-java.logger") +local nio = require("nio") + +local function coc_command(id, params, _) + local 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, + { + -- coc adapter table for the native lsp client + 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, +}