diff --git a/lua/lazyvim/plugins/extras/lang/java.lua b/lua/lazyvim/plugins/extras/lang/java.lua index d7e7f91e..64808bee 100644 --- a/lua/lazyvim/plugins/extras/lang/java.lua +++ b/lua/lazyvim/plugins/extras/lang/java.lua @@ -1,15 +1,33 @@ +local Util = require("lazyvim.util") + +-- This is the same as in lspconfig.server_configurations.jdtls, but avoids +-- needing to require that when this module loads. +local java_filetypes = { "java" } + +-- Utility function to extend or override a config table, similar to the way +-- that Plugin.opts works. +---@param config table +---@param custom function | table | nil +local function extend_or_override(config, custom, ...) + if type(custom) == "function" then + config = custom(config, ...) or config + elseif custom then + config = vim.tbl_deep_extend("force", config, custom) --[[@as table]] + end + return config +end + return { -- Add java to treesitter. { "nvim-treesitter/nvim-treesitter", opts = function(_, opts) - if type(opts.ensure_installed) == "table" then - vim.list_extend(opts.ensure_installed, { "java" }) - end + opts.ensure_installed = opts.ensure_installed or {} + vim.list_extend(opts.ensure_installed, { "java" }) end, }, - -- Ensure java debugger and test packages are installed + -- Ensure java debugger and test packages are installed. { "mfussenegger/nvim-dap", optional = true, @@ -24,102 +42,186 @@ return { }, }, - -- Set up lsp with mfussenegger/nvim-jdtls instead of nvim-lspconfig. + -- Configure nvim-lspconfig to install the server automatically via mason, but + -- defer actually starting it to our configuration of nvim-jtdls below. { "neovim/nvim-lspconfig", - dependencies = { "folke/which-key.nvim", "mfussenegger/nvim-jdtls" }, opts = { -- make sure mason installs the server servers = { - jdtls = { - -- stylua: ignore - keys = { - { "co", function() require("jdtls").organize_imports() end, desc = "Organize Imports", }, - { "cR", function() require("jdtls").rename_file() end, desc = "Rename File", }, - { "cxv", function() require("jdtls").extract_variable() end, desc = "Extract Variable", }, - { "cxv", function() require("jdtls").extract_variable({ visual = true }) end, mode = "v", desc = "Extract Variable", }, - { "cxc", function() require("jdtls").extract_constant() end, desc = "Extract Constant", }, - { "cxc", function() require("jdtls").extract_constant({ visual = true }) end, mode = "v", desc = "Extract Constant", }, - { "cxm", function() require("jdtls").extract_method({ visual = true }) end, mode = "v", desc = "Extract Method", }, - }, - }, + jdtls = {}, }, setup = { - -- Override the default jdtls server startup to use nvim-jdtls. jdtls = function() - -- The lspconfig configuration for jdtls contains two useful items: - -- 1. The list of filetypes on which to match. - -- 2. Custom method for finding the root for a java project. - local lsp_config = require("lspconfig.server_configurations.jdtls").default_config - local find_java_project_root = lsp_config.root_dir - local filetypes = lsp_config.filetypes - -- lookup paths for java test and debugger package - local mason_registry = require("mason-registry") - local bundles = {} - if mason_registry.has_package("java-test") and mason_registry.has_package("java-debug-adapter") then - -- jdtls tools configuration for debugging support - local java_test_pkg = mason_registry.get_package("java-test") - local java_test_path = java_test_pkg:get_install_path() - local java_dbg_pkg = mason_registry.get_package("java-debug-adapter") - local java_dbg_path = java_dbg_pkg:get_install_path() - local jar_patterns = { - java_dbg_path .. "/extension/server/com.microsoft.java.debug.plugin-*.jar", - java_test_path .. "/extension/server/*.jar" - } - for _, jar_pattern in ipairs(jar_patterns) do - for _, bundle in ipairs(vim.split(vim.fn.glob(jar_pattern), '\n')) do - table.insert(bundles, bundle) - end - end - end - -- Attach jdtls for the proper filetypes (i.e. java). - -- Existing server will be reused if the root_dir matches. - vim.api.nvim_create_autocmd("FileType", { - group = vim.api.nvim_create_augroup("MyJdtls", { clear = true }), - pattern = filetypes, - callback = function() - local fname = vim.api.nvim_buf_get_name(0) - local root_dir = find_java_project_root(fname) - local project_name = root_dir and vim.fs.basename(root_dir) - local cmd = { "jdtls" } - if project_name then - local jdtls_cache_dir = vim.fn.stdpath("cache") .. "/jdtls/" .. project_name - vim.list_extend(cmd, { - "-configuration", - jdtls_cache_dir .. "/config", - "-data", - jdtls_cache_dir .. "/workspace", - }) - end - local jdtls_base_config = { - on_attach = require("lazyvim.util").on_attach(function(client, buffer) - if mason_registry.has_package("java-test") then - -- custom keymaps for Java test runner (not yet compatible with neotest) - vim.keymap.set("n", "tT", function() require("jdtls").pick_test({ bufnr = buffer }) end, { buffer = buffer, desc = "Run specific Test" }) - vim.keymap.set("n", "tt", function() require("jdtls").test_class({ bufnr = buffer }) end, { buffer = buffer, desc = "Run File" }) - vim.keymap.set("n", "tr", function() require("jdtls").test_nearest_method({ bufnr = buffer }) end, { buffer = buffer, desc = "Run nearest" }) - end - if mason_registry.has_package("java-debug-adapter") then - -- custom init for Java debugger - require("jdtls").setup_dap({ hotcodereplace = "auto" }) - require("jdtls.dap").setup_dap_main_class_configs() - end - require("jdtls.setup").add_commands() - end), - cmd = cmd, - root_dir = root_dir, - init_options = { - bundles = bundles, - } - } - local jdtls_opts = require("lazyvim.util").opts("nvim-jdtls") - require("jdtls").start_or_attach(vim.tbl_deep_extend("force", jdtls_opts or {}, jdtls_base_config)) - require("which-key").register({ c = { x = { name = "Extract" } } }, { prefix = "" }) - end, - }) return true -- avoid duplicate servers end, }, }, }, + + -- Set up nvim-jdtls to attach to java files. + { + "mfussenegger/nvim-jdtls", + dependencies = { "folke/which-key.nvim" }, + ft = java_filetypes, + opts = function() + return { + -- How to find the root dir for a given filename. The default comes from + -- lspconfig which provides a function specifically for java projects. + root_dir = require("lspconfig.server_configurations.jdtls").default_config.root_dir, + + -- How to find the project name for a given root dir. + project_name = function(root_dir) + return root_dir and vim.fs.basename(root_dir) + end, + + -- Where are the config and workspace dirs for a project? + jdtls_config_dir = function(project_name) + return vim.fn.stdpath("cache") .. "/jdtls/" .. project_name .. "/config" + end, + jdtls_workspace_dir = function(project_name) + return vim.fn.stdpath("cache") .. "/jdtls/" .. project_name .. "/workspace" + end, + + -- How to run jdtls. This can be overridden to a full java command-line + -- if the Python wrapper script doesn't suffice. + cmd = { "jdtls" }, + full_cmd = function(opts) + local fname = vim.api.nvim_buf_get_name(0) + local root_dir = opts.root_dir(fname) + local project_name = opts.project_name(root_dir) + local cmd = vim.deepcopy(opts.cmd) + if project_name then + vim.list_extend(cmd, { + "-configuration", + opts.jdtls_config_dir(project_name), + "-data", + opts.jdtls_workspace_dir(project_name), + }) + end + return cmd + end, + + -- These depend on nvim-dap, but can additionally be disabled by setting false here. + dap = { hotcodereplace = "auto", config_overrides = {} }, + test = true, + } + end, + config = function() + local opts = Util.opts("nvim-jdtls") or {} + + -- Find the extra bundles that should be passed on the jdtls command-line + -- if nvim-dap is enabled with java debug/test. + local mason_registry = require("mason-registry") + local bundles = {} ---@type string[] + if opts.dap and Util.has("nvim-dap") and mason_registry.is_installed("java-debug-adapter") then + local java_dbg_pkg = mason_registry.get_package("java-debug-adapter") + local java_dbg_path = java_dbg_pkg:get_install_path() + local jar_patterns = { + java_dbg_path .. "/extension/server/com.microsoft.java.debug.plugin-*.jar", + } + -- java-test also depends on java-debug-adapter. + if opts.test and mason_registry.is_installed("java-test") then + local java_test_pkg = mason_registry.get_package("java-test") + local java_test_path = java_test_pkg:get_install_path() + vim.list_extend(jar_patterns, { + java_test_path .. "/extension/server/*.jar", + }) + end + for _, jar_pattern in ipairs(jar_patterns) do + for _, bundle in ipairs(vim.split(vim.fn.glob(jar_pattern), "\n")) do + table.insert(bundles, bundle) + end + end + end + + local function attach_jdtls() + local fname = vim.api.nvim_buf_get_name(0) + + -- Configuration can be augmented and overridden by opts.jdtls + local config = extend_or_override({ + cmd = opts.full_cmd(opts), + root_dir = opts.root_dir(fname), + init_options = { + bundles = bundles, + }, + -- enable CMP capabilities + capabilities = require("cmp_nvim_lsp").default_capabilities(), + }, opts.jdtls) + + -- Existing server will be reused if the root_dir matches. + require("jdtls").start_or_attach(config) + -- not need to require("jdtls.setup").add_commands(), start automatically adds commands + end + + -- Attach the jdtls for each java buffer. HOWEVER, this plugin loads + -- depending on filetype, so this autocmd doesn't run for the first file. + -- For that, we call directly below. + vim.api.nvim_create_autocmd("FileType", { + pattern = java_filetypes, + callback = attach_jdtls, + }) + + -- Setup keymap and dap after the lsp is fully attached. + -- https://github.com/mfussenegger/nvim-jdtls#nvim-dap-configuration + -- https://neovim.io/doc/user/lsp.html#LspAttach + vim.api.nvim_create_autocmd("LspAttach", { + callback = function(args) + local client = vim.lsp.get_client_by_id(args.data.client_id) + if client and client.name == "jdtls" then + local wk = require("which-key") + wk.register({ + ["cx"] = { name = "+extract" }, + ["cxv"] = { require("jdtls").extract_variable_all, "Extract Variable" }, + ["cxc"] = { require("jdtls").extract_constant, "Extract Constant" }, + ["gs"] = { require("jdtls").super_implementation, "Goto Super" }, + ["gS"] = { require("jdtls.tests").goto_subjects, "Goto Subjects" }, + ["co"] = { require("jdtls").organize_imports, "Organize Imports" }, + }, { mode = "n", buffer = args.buf }) + wk.register({ + ["c"] = { name = "+code" }, + ["cx"] = { name = "+extract" }, + ["cxm"] = { + [[lua require('jdtls').extract_method(true)]], + "Extract Method", + }, + ["cxv"] = { + [[lua require('jdtls').extract_variable_all(true)]], + "Extract Variable", + }, + ["cxc"] = { + [[lua require('jdtls').extract_constant(true)]], + "Extract Constant", + }, + }, { mode = "v", buffer = args.buf }) + + if opts.dap and Util.has("nvim-dap") and mason_registry.is_installed("java-debug-adapter") then + -- custom init for Java debugger + require("jdtls").setup_dap(opts.dap) + require("jdtls.dap").setup_dap_main_class_configs() + + -- Java Test require Java debugger to work + if opts.test and mason_registry.is_installed("java-test") then + -- custom keymaps for Java test runner (not yet compatible with neotest) + wk.register({ + ["t"] = { name = "+test" }, + ["tt"] = { require("jdtls.dap").test_class, "Run All Test" }, + ["tr"] = { require("jdtls.dap").test_nearest_method, "Run Nearest Test" }, + ["tT"] = { require("jdtls.dap").pick_test, "Run Test" }, + }, { mode = "n", buffer = args.buf }) + end + end + + -- User can set additional keymaps in opts.on_attach + if opts.on_attach then + opts.on_attach(args) + end + end + end, + }) + + -- Avoid race condition by calling attach the first time, since the autocmd won't fire. + attach_jdtls() + end, + }, }