diff --git a/README.md b/README.md index d7776b2..c6217a2 100644 --- a/README.md +++ b/README.md @@ -336,33 +336,44 @@ require('java').setup({ -- JDTLS configuration jdtls = { version = '1.43.0', + path = nil, + auto_install = true, }, -- Extensions lombok = { enable = true, version = '1.18.40', + path = nil, + auto_install = true, }, java_test = { enable = true, version = '0.40.1', + path = nil, + auto_install = true, }, java_debug_adapter = { enable = true, version = '0.58.2', + path = nil, + auto_install = true, }, spring_boot_tools = { enable = true, version = '1.55.1', + path = nil, + auto_install = true, }, -- JDK installation jdk = { auto_install = true, version = '17', + path = nil, }, -- Logging @@ -377,6 +388,20 @@ require('java').setup({ }) ``` +Set `path` when a tool is managed externally. When `path` is set, nvim-java +uses that path and does not install the tool. Set +`auto_install = false` on a tool to fail instead of downloading when no path is +configured. + +Path meanings: + +- `jdtls.path`: directory containing `plugins/` and platform `config_*` + directories +- `lombok.path`: path to `lombok.jar` +- `java_test.path`, `java_debug_adapter.path`, `spring_boot_tools.path`: VS Code + extension root containing `package.json` +- `jdk.path`: JDK home containing `bin/java` + ## :golf: Architecture diff --git a/doc/nvim-java.txt b/doc/nvim-java.txt index 709c13b..579a90c 100644 --- a/doc/nvim-java.txt +++ b/doc/nvim-java.txt @@ -349,33 +349,44 @@ want, following options are available: -- JDTLS configuration jdtls = { version = '1.43.0', + path = nil, + auto_install = true, }, -- Extensions lombok = { enable = true, version = '1.18.40', + path = nil, + auto_install = true, }, java_test = { enable = true, version = '0.40.1', + path = nil, + auto_install = true, }, java_debug_adapter = { enable = true, version = '0.58.2', + path = nil, + auto_install = true, }, spring_boot_tools = { enable = true, version = '1.55.1', + path = nil, + auto_install = true, }, -- JDK installation jdk = { auto_install = true, version = '17', + path = nil, }, -- Logging @@ -390,6 +401,20 @@ want, following options are available: }) < +Set `path` when a tool is managed externally. When `path` is set, nvim-java +uses that path and does not install the tool. Set +`auto_install = false` on a tool to fail instead of downloading when no path is +configured. + +Path meanings: + +- `jdtls.path`: directory containing `plugins/` and platform `config_*` + directories +- `lombok.path`: path to `lombok.jar` +- `java_test.path`, `java_debug_adapter.path`, `spring_boot_tools.path`: VS Code + extension root containing `package.json` +- `jdk.path`: JDK home containing `bin/java` + ARCHITECTURE *nvim-java-architecture* diff --git a/lua/java-core/ls/servers/jdtls/cmd.lua b/lua/java-core/ls/servers/jdtls/cmd.lua index bc63ec2..1379822 100644 --- a/lua/java-core/ls/servers/jdtls/cmd.lua +++ b/lua/java-core/ls/servers/jdtls/cmd.lua @@ -1,12 +1,12 @@ local List = require('java-core.utils.list') local path = require('java-core.utils.path') -local Manager = require('pkgm.manager') local system = require('java-core.utils.system') local log = require('java-core.utils.log2') local err = require('java-core.utils.errors') local java_version_map = require('java-core.constants.java_version') local lsp_utils = require('java-core.utils.lsp') local str = require('java-core.utils.str') +local resolver = require('pkgm.resolve') local M = {} @@ -23,7 +23,7 @@ function M.get_cmd(config) -- system java. So as a workaround, we use the absolute path to java instead -- So following check is not needed when we have auto_install set to true -- @see https://github.com/neovim/neovim/issues/36818 - if not config.jdk.auto_install then + if not config.jdk.auto_install and not config.jdk.path then M.validate_java_version(config, lsp_config.cmd_env) end @@ -44,23 +44,16 @@ end ---@return java-core.List function M.get_jvm_args(config) local use_lombok = config.lombok.enable - local jdtls_root = Manager:get_install_dir('jdtls', config.jdtls.version) - local jdtls_config = path.join(jdtls_root, system.get_config_suffix()) + local jdtls_root = resolver.get_jdtls_root(config) + local jdtls_config = M.get_jdtls_config_path(jdtls_root) local java_exe = 'java' -- NOTE: eventhough we are setting the PATH env var, due to a bug, it's not -- working on Windows. So we are using the absolute path to java instead -- @see https://github.com/neovim/neovim/issues/36818 - if config.jdk.auto_install then - local jdk_root = Manager:get_install_dir('openjdk', config.jdk.version) - local java_home - if system.get_os() == 'mac' then - java_home = vim.fn.glob(path.join(jdk_root, 'jdk-*', 'Contents', 'Home')) - else - java_home = vim.fn.glob(path.join(jdk_root, 'jdk-*')) - end - + if config.jdk.auto_install or config.jdk.path then + local java_home = resolver.get_jdk_home(config) java_exe = path.join(java_home, 'bin', 'java') end @@ -85,20 +78,37 @@ function M.get_jvm_args(config) -- Adding lombok if use_lombok then - local lombok_root = Manager:get_install_dir('lombok', config.lombok.version) - local lombok_path = vim.fn.glob(path.join(lombok_root, 'lombok*.jar')) - jvm_args:push('-javaagent:' .. lombok_path) + jvm_args:push('-javaagent:' .. resolver.get_lombok_path(config)) end return jvm_args end +---@private +---@param jdtls_root string +---@return string +function M.get_jdtls_config_path(jdtls_root) + local config_path = path.join(jdtls_root, system.get_config_suffix()) + + if vim.fn.isdirectory(config_path) == 1 then + return config_path + end + + local os_config_path = path.join(jdtls_root, 'config_' .. system.get_os()) + + if vim.fn.isdirectory(os_config_path) == 1 then + return os_config_path + end + + return config_path +end + ---@private ---@param config java.Config ---@param cwd? string ---@return java-core.List function M.get_jar_args(config, cwd) - local jdtls_root = Manager:get_install_dir('jdtls', config.jdtls.version) + local jdtls_root = resolver.get_jdtls_root(config) cwd = cwd or vim.fn.getcwd() local launcher_reg = path.join(jdtls_root, 'plugins', 'org.eclipse.equinox.launcher_*.jar') @@ -115,7 +125,7 @@ function M.get_jar_args(config, cwd) equinox_launcher, '-configuration', - lsp_utils.get_jdtls_cache_conf_path(), + lsp_utils.get_jdtls_cache_conf_path(jdtls_root), '-data', lsp_utils.get_jdtls_cache_data_path(cwd), diff --git a/lua/java-core/ls/servers/jdtls/env.lua b/lua/java-core/ls/servers/jdtls/env.lua index 2c6ab81..1f3d59e 100644 --- a/lua/java-core/ls/servers/jdtls/env.lua +++ b/lua/java-core/ls/servers/jdtls/env.lua @@ -1,26 +1,18 @@ local path = require('java-core.utils.path') -local Manager = require('pkgm.manager') local log = require('java-core.utils.log2') local system = require('java-core.utils.system') +local resolver = require('pkgm.resolve') local M = {} --- @param config java.Config function M.get_env(config) - if not config.jdk.auto_install then - log.debug('config.jdk.auto_install disabled, returning empty env') + if not config.jdk.auto_install and not config.jdk.path then + log.debug('config.jdk.auto_install disabled and config.jdk.path unset, returning empty env') return {} end - local jdk_root = Manager:get_install_dir('openjdk', config.jdk.version) - - local java_home - - if system.get_os() == 'mac' then - java_home = vim.fn.glob(path.join(jdk_root, 'jdk-*', 'Contents', 'Home')) - else - java_home = vim.fn.glob(path.join(jdk_root, 'jdk-*')) - end + local java_home = resolver.get_jdk_home(config) local java_bin = path.join(java_home, 'bin') diff --git a/lua/java-core/ls/servers/jdtls/plugins.lua b/lua/java-core/ls/servers/jdtls/plugins.lua index a66ce5b..3cc7e0e 100644 --- a/lua/java-core/ls/servers/jdtls/plugins.lua +++ b/lua/java-core/ls/servers/jdtls/plugins.lua @@ -1,10 +1,10 @@ local M = {} -function M.get_plugin_version_map(config) +function M.get_plugin_config_map(config) return { - ['java-test'] = config.java_test.version, - ['java-debug'] = config.java_debug_adapter.version, - ['spring-boot-tools'] = config.spring_boot_tools.version, + ['java-test'] = config.java_test, + ['java-debug'] = config.java_debug_adapter, + ['spring-boot-tools'] = config.spring_boot_tools, } end @@ -15,19 +15,18 @@ end function M.get_plugins(config, plugins) local file = require('java-core.utils.file') local List = require('java-core.utils.list') - local Manager = require('pkgm.manager') local path = require('java-core.utils.path') local err = require('java-core.utils.errors') local str = require('java-core.utils.str') + local resolver = require('pkgm.resolve') - local plugin_version_map = M.get_plugin_version_map(config) + local plugin_config_map = M.get_plugin_config_map(config) return List:new(plugins) :map(function(plugin_name) - local version = plugin_version_map[plugin_name] + local plugin_config = plugin_config_map[plugin_name] - local pkg_path = Manager:get_install_dir(plugin_name, version) - local plugin_root = path.join(pkg_path, 'extension') + local plugin_root = resolver.get_extension_root(plugin_name, plugin_config) local package_json_str = vim.fn.readfile(path.join(plugin_root, 'package.json')) local package_json = vim.json.decode(table.concat(package_json_str, '\n')) local java_extensions = package_json.contributes.javaExtensions diff --git a/lua/java-core/utils/lsp.lua b/lua/java-core/utils/lsp.lua index af15372..bc359ee 100644 --- a/lua/java-core/utils/lsp.lua +++ b/lua/java-core/utils/lsp.lua @@ -23,12 +23,23 @@ function M.get_jdtls_cache_root_path() return cache_root end +local function get_cache_key(value) + if value == nil or value == '' then + return nil + end + + return vim.fn.sha256(value) +end + --- Returns the path to the jdtls config file +---@param jdtls_root? string ---@return string -function M.get_jdtls_cache_conf_path() +function M.get_jdtls_cache_conf_path(jdtls_root) local path = require('java-core.utils.path') local cache_root = M.get_jdtls_cache_root_path() - local conf_path = path.join(cache_root, 'config') + local cache_key = get_cache_key(jdtls_root) + local conf_dir_name = cache_key and ('config_' .. cache_key) or 'config' + local conf_path = path.join(cache_root, conf_dir_name) return conf_path end diff --git a/lua/java.lua b/lua/java.lua index 59332f3..f7aa13c 100644 --- a/lua/java.lua +++ b/lua/java.lua @@ -28,16 +28,15 @@ function M.setup(custom_config) ---------------------------------------------------------------------- -- package installation -- ---------------------------------------------------------------------- - local Manager = require('pkgm.manager') - local pkgm = Manager() + local pkgm = require('pkgm.resolve') - pkgm:install('jdtls', config.jdtls.version) + pkgm.install('jdtls', config.jdtls) if config.java_test.enable then ---------------------------------------------------------------------- -- test -- ---------------------------------------------------------------------- - pkgm:install('java-test', config.java_test.version) + pkgm.install('java-test', config.java_test) M.test = { run_current_class = test_api.run_current_class, @@ -54,7 +53,7 @@ function M.setup(custom_config) ---------------------------------------------------------------------- -- debugger -- ---------------------------------------------------------------------- - pkgm:install('java-debug', config.java_debug_adapter.version) + pkgm.install('java-debug', config.java_debug_adapter) require('java-dap').setup() M.dap = { @@ -65,15 +64,15 @@ function M.setup(custom_config) end if config.spring_boot_tools.enable then - pkgm:install('spring-boot-tools', config.spring_boot_tools.version) + pkgm.install('spring-boot-tools', config.spring_boot_tools) end if config.lombok.enable then - pkgm:install('lombok', config.lombok.version) + pkgm.install('lombok', config.lombok) end if config.jdk.auto_install then - pkgm:install('openjdk', config.jdk.version) + pkgm.install('openjdk', config.jdk) end ---------------------------------------------------------------------- diff --git a/lua/java/config.lua b/lua/java/config.lua index 8b65edc..3891509 100644 --- a/lua/java/config.lua +++ b/lua/java/config.lua @@ -21,22 +21,22 @@ local V = jdtls_version_map[JDTLS_VERSION] ---@class java.Config ---@field checks { nvim_version: boolean, nvim_jdtls_conflict: boolean } ----@field jdtls { version: string } ----@field lombok { enable: boolean, version: string } ----@field java_test { enable: boolean, version: string } ----@field java_debug_adapter { enable: boolean, version: string } ----@field spring_boot_tools { enable: boolean, version: string } ----@field jdk { auto_install: boolean, version: string } +---@field jdtls { version: string, path: string|nil, auto_install: boolean } +---@field lombok { enable: boolean, version: string, path: string|nil, auto_install: boolean } +---@field java_test { enable: boolean, version: string, path: string|nil, auto_install: boolean } +---@field java_debug_adapter { enable: boolean, version: string, path: string|nil, auto_install: boolean } +---@field spring_boot_tools { enable: boolean, version: string, path: string|nil, auto_install: boolean } +---@field jdk { auto_install: boolean, version: string, path: string|nil } ---@field log java-core.Log2Config ---@class java.PartialConfig ---@field checks? { nvim_version?: boolean, nvim_jdtls_conflict?: boolean } ----@field jdtls? { version?: string } ----@field lombok? { enable?: boolean, version?: string } ----@field java_test? { enable?: boolean, version?: string } ----@field java_debug_adapter? { enable?: boolean, version?: string } ----@field spring_boot_tools? { enable?: boolean, version?: string } ----@field jdk? { auto_install?: boolean, version?: string } +---@field jdtls? { version?: string, path?: string, auto_install?: boolean } +---@field lombok? { enable?: boolean, version?: string, path?: string, auto_install?: boolean } +---@field java_test? { enable?: boolean, version?: string, path?: string, auto_install?: boolean } +---@field java_debug_adapter? { enable?: boolean, version?: string, path?: string, auto_install?: boolean } +---@field spring_boot_tools? { enable?: boolean, version?: string, path?: string, auto_install?: boolean } +---@field jdk? { auto_install?: boolean, version?: string, path?: string } ---@field log? java-core.PartialLog2Config ---@type java.Config @@ -48,33 +48,44 @@ local config = { jdtls = { version = JDTLS_VERSION, + path = nil, + auto_install = true, }, lombok = { enable = true, version = V.lombok, + path = nil, + auto_install = true, }, -- load java test plugins java_test = { enable = true, version = V.java_test, + path = nil, + auto_install = true, }, -- load java debugger plugins java_debug_adapter = { enable = true, version = V.java_debug_adapter, + path = nil, + auto_install = true, }, spring_boot_tools = { enable = true, version = V.spring_boot_tools, + path = nil, + auto_install = true, }, jdk = { auto_install = true, version = V.jdk, + path = nil, }, log = { diff --git a/lua/java/startup/lsp_setup.lua b/lua/java/startup/lsp_setup.lua index 49e5cbd..9a82621 100644 --- a/lua/java/startup/lsp_setup.lua +++ b/lua/java/startup/lsp_setup.lua @@ -1,6 +1,6 @@ local path = require('java-core.utils.path') local List = require('java-core.utils.list') -local Manager = require('pkgm.manager') +local resolver = require('pkgm.resolve') local server = require('java-core.ls.servers.jdtls') @@ -22,10 +22,10 @@ function M.setup(config) if config.spring_boot_tools.enable then jdtls_plugins:push('spring-boot-tools') - local spring_boot_root = Manager:get_install_dir('spring-boot-tools', config.spring_boot_tools.version) + local spring_boot_root = resolver.get_extension_root('spring-boot-tools', config.spring_boot_tools) require('spring_boot').setup({ - ls_path = path.join(spring_boot_root, 'extension', 'language-server'), + ls_path = path.join(spring_boot_root, 'language-server'), }) require('spring_boot').init_lsp_commands() diff --git a/lua/pkgm/resolve.lua b/lua/pkgm/resolve.lua new file mode 100644 index 0000000..d9d24b0 --- /dev/null +++ b/lua/pkgm/resolve.lua @@ -0,0 +1,86 @@ +local Manager = require('pkgm.manager') +local path = require('java-core.utils.path') +local err = require('java-core.utils.errors') +local system = require('java-core.utils.system') + +local M = {} + +local function has_path(config) + return config.path ~= nil and config.path ~= '' +end + +local function can_auto_install(config) + return config.auto_install ~= false +end + +---@param name string +---@param config { version: string, path?: string, auto_install?: boolean } +---@return string +function M.install(name, config) + if has_path(config) then + return config.path + end + + if not can_auto_install(config) then + err.throw(('nvim-java: %s auto_install disabled and no path configured'):format(name)) + end + + return Manager():install(name, config.version) +end + +---@param name string +---@param config { version: string, path?: string } +---@return string +function M.get_install_dir(name, config) + if has_path(config) then + return config.path + end + + return Manager:get_install_dir(name, config.version) +end + +---@param name string +---@param config { version: string, path?: string } +---@return string +function M.get_extension_root(name, config) + if has_path(config) then + return config.path + end + + return path.join(M.get_install_dir(name, config), 'extension') +end + +---@param config java.Config +---@return string +function M.get_jdtls_root(config) + return M.get_install_dir('jdtls', config.jdtls) +end + +---@param config java.Config +---@return string +function M.get_lombok_path(config) + if has_path(config.lombok) then + return config.lombok.path + end + + local lombok_root = M.get_install_dir('lombok', config.lombok) + return vim.fn.glob(path.join(lombok_root, 'lombok*.jar')) +end + +---@param config java.Config +---@return string +function M.get_jdk_home(config) + if has_path(config.jdk) then + return config.jdk.path + end + + local jdk_root = M.get_install_dir('openjdk', config.jdk) + + if system.get_os() == 'mac' then + return vim.fn.glob(path.join(jdk_root, 'jdk-*', 'Contents', 'Home')) + end + + return vim.fn.glob(path.join(jdk_root, 'jdk-*')) +end + +return M diff --git a/tests/specs/jdtls_cmd_spec.lua b/tests/specs/jdtls_cmd_spec.lua new file mode 100644 index 0000000..a0e1678 --- /dev/null +++ b/tests/specs/jdtls_cmd_spec.lua @@ -0,0 +1,31 @@ +local assert = require('luassert') + +describe('JDTLS command', function() + local cmd = require('java-core.ls.servers.jdtls.cmd') + local path = require('java-core.utils.path') + + local temp_dir + + before_each(function() + temp_dir = vim.fn.tempname() + vim.fn.mkdir(temp_dir, 'p') + end) + + after_each(function() + vim.fn.delete(temp_dir, 'rf') + end) + + it('uses the platform-specific config directory when it exists', function() + local config_dir = path.join(temp_dir, 'config_mac_arm') + vim.fn.mkdir(config_dir, 'p') + + assert.equals(config_dir, cmd.get_jdtls_config_path(temp_dir)) + end) + + it('falls back to the os config directory when the platform-specific directory is missing', function() + local config_dir = path.join(temp_dir, 'config_mac') + vim.fn.mkdir(config_dir, 'p') + + assert.equals(config_dir, cmd.get_jdtls_config_path(temp_dir)) + end) +end) diff --git a/tests/specs/lsp_utils_spec.lua b/tests/specs/lsp_utils_spec.lua new file mode 100644 index 0000000..06033da --- /dev/null +++ b/tests/specs/lsp_utils_spec.lua @@ -0,0 +1,21 @@ +local assert = require('luassert') + +describe('LSP utils', function() + it('uses a stable jdtls-root specific config cache path', function() + local lsp = require('java-core.utils.lsp') + local cache_root = vim.fn.stdpath('cache') .. '/jdtls' + local jdtls_root = '/nix/store/example-jdtls/share/java/jdtls' + + local conf_path = lsp.get_jdtls_cache_conf_path(jdtls_root) + + assert.equals(cache_root .. '/config_' .. vim.fn.sha256(jdtls_root), conf_path) + end) + + it('keeps the legacy config cache path when no jdtls root is provided', function() + local lsp = require('java-core.utils.lsp') + + local conf_path = lsp.get_jdtls_cache_conf_path() + + assert.equals(vim.fn.stdpath('cache') .. '/jdtls/config', conf_path) + end) +end) diff --git a/tests/specs/pkgm_resolve_spec.lua b/tests/specs/pkgm_resolve_spec.lua new file mode 100644 index 0000000..3371c98 --- /dev/null +++ b/tests/specs/pkgm_resolve_spec.lua @@ -0,0 +1,124 @@ +local assert = require('luassert') + +describe('Package resolver', function() + local resolver + local Manager + local original_install + local original_get_install_dir + + before_each(function() + package.loaded['pkgm.resolve'] = nil + + Manager = require('pkgm.manager') + original_install = Manager.install + original_get_install_dir = Manager.get_install_dir + + resolver = require('pkgm.resolve') + end) + + after_each(function() + Manager.install = original_install + Manager.get_install_dir = original_get_install_dir + end) + + it('uses configured paths without installing packages', function() + Manager.install = function() + error('install should not be called') + end + + local install_dir = resolver.install('jdtls', { + version = '1.54.0', + path = '/opt/jdtls', + }) + + assert.equals('/opt/jdtls', install_dir) + end) + + it('installs packages when no path is configured and auto_install is enabled', function() + local installed_name + local installed_version + + Manager.install = function(_, name, version) + installed_name = name + installed_version = version + return '/downloaded/' .. name .. '/' .. version + end + + local install_dir = resolver.install('jdtls', { + version = '1.54.0', + auto_install = true, + }) + + assert.equals('jdtls', installed_name) + assert.equals('1.54.0', installed_version) + assert.equals('/downloaded/jdtls/1.54.0', install_dir) + end) + + it('fails when auto_install is disabled and no path is configured', function() + local log = require('java-core.utils.log2') + local original_notify = vim.notify + local original_log_error = log.error + + vim.notify = function() end + log.error = function() end + + local ok, assertion_err = pcall(function() + assert.has_error(function() + resolver.install('jdtls', { + version = '1.54.0', + auto_install = false, + }) + end, 'nvim-java: jdtls auto_install disabled and no path configured') + end) + + vim.notify = original_notify + log.error = original_log_error + + if not ok then + error(assertion_err) + end + end) + + it('resolves external vscode extension roots directly', function() + local extension_root = resolver.get_extension_root('java-test', { + version = '0.43.2', + path = '/opt/vscode-java-test', + }) + + assert.equals('/opt/vscode-java-test', extension_root) + end) + + it('resolves downloaded vscode extension roots from the package layout', function() + Manager.get_install_dir = function(_, name, version) + return '/downloaded/' .. name .. '/' .. version + end + + local extension_root = resolver.get_extension_root('java-test', { + version = '0.43.2', + }) + + assert.equals('/downloaded/java-test/0.43.2/extension', extension_root) + end) + + it('uses exact lombok jar paths when configured', function() + local lombok_path = resolver.get_lombok_path({ + lombok = { + version = '1.18.42', + path = '/opt/lombok.jar', + }, + }) + + assert.equals('/opt/lombok.jar', lombok_path) + end) + + it('uses jdk paths as java home when configured', function() + local jdk_home = resolver.get_jdk_home({ + jdk = { + version = '25', + path = '/opt/jdk', + }, + }) + + assert.equals('/opt/jdk', jdk_home) + end) +end)