Hi Nico, for the last few weeks I have been trying to get Java debugging to work in Neovim. Last night I got it to work, and it does not involve a very specific installation of neovim to get working.
Firstly, lets start with the plugins I use that relate to debugging. For package management I use packer.
In my plugins file packer.lua
return require('packer').startup(function(use)
use 'wbthomason/packer.nvim'
use { -- better notifications, cmdline, popupmenu
'folke/noice.nvim',
requires = {
"MunifTanjim/nui.nvim",
"rcarriga/nvim-notify",
}
}
use 'mfussenegger/nvim-jdtls' -- Java LSP
use 'mfussenegger/nvim-dap' -- Nvim dap
use 'rcarriga/nvim-dap-ui' -- Better UI for DAP
use 'theHamsta/nvim-dap-virtual-text' -- Better UI for DAP
use ('nvim-treesitter/nvim-treesitter', {run = ':TSUpdate'}) -- Syntax highlighting
use { -- LSP installers, managers, and completion
'VonHeikemen/lsp-zero.nvim',
branch = 'v2.x',
requires = {
-- LSP Support
{'neovim/nvim-lspconfig'}, -- Required
{ -- Optional
'williamboman/mason.nvim',
run = function()
pcall(vim.cmd, 'MasonUpdate')
end,
},
{'williamboman/mason-lspconfig.nvim'}, -- Optional
-- Autocompletion
{'hrsh7th/nvim-cmp'}, -- Required
{'hrsh7th/cmp-nvim-lsp'}, -- Required
{'L3MON4D3/LuaSnip'}, -- Required
}
}
end)
In my nvim/after/plugin
directory (which just runs lua files, see :h after-directory
for more info) I have a file named jdtls.lua
, which could be named something else as it is simply a lua file which contains my Java LSP configuration information.
jdtls.lua
local java_cmds = vim.api.nvim_create_augroup('java_cmds', {clear = true})
local cache_vars = {}
local root_files = {
'.git',
'mvnw',
'gradlew',
'pom.xml',
'build.gradle',
}
local features = {
-- change this to `true` to enable codelens
codelens = false,
-- change this to `true` if you have `nvim-dap`,
-- `java-test` and `java-debug-adapter` installed
debugger = true,
}
local function get_jdtls_paths()
if cache_vars.paths then
return cache_vars.paths
end
local path = {}
path.data_dir = vim.fn.stdpath('cache') .. '/nvim-jdtls'
local jdtls_install = require('mason-registry')
.get_package('jdtls')
:get_install_path()
path.java_agent = jdtls_install .. '/lombok.jar'
path.launcher_jar = vim.fn.glob(jdtls_install .. '/plugins/org.eclipse.equinox.launcher_*.jar')
if vim.fn.has('mac') == 1 then
path.platform_config = jdtls_install .. '/config_mac'
elseif vim.fn.has('unix') == 1 then
path.platform_config = jdtls_install .. '/config_linux'
elseif vim.fn.has('win32') == 1 then
path.platform_config = jdtls_install .. '/config_win'
end
path.bundles = {}
---
-- Include java-test bundle if present
---
local java_test_path = require('mason-registry')
.get_package('java-test')
:get_install_path()
local java_test_bundle = vim.split(
vim.fn.glob(java_test_path .. '/extension/server/*.jar'),
'\n'
)
if java_test_bundle[1] ~= '' then
vim.list_extend(path.bundles, java_test_bundle)
end
---
-- Include java-debug-adapter bundle if present
---
local java_debug_path = require('mason-registry')
.get_package('java-debug-adapter')
:get_install_path()
local java_debug_bundle = vim.split(
vim.fn.glob(java_debug_path .. '/extension/server/com.microsoft.java.debug.plugin-*.jar'),
'\n'
)
if java_debug_bundle[1] ~= '' then
vim.list_extend(path.bundles, java_debug_bundle)
end
---
-- Useful if you're starting jdtls with a Java version that's
-- different from the one the project uses.
---
path.runtimes = {
-- Note: the field `name` must be a valid `ExecutionEnvironment`,
-- you can find the list here:
-- https://github.com/eclipse/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request
--
-- This example assume you are using sdkman: https://sdkman.io
-- {
-- name = 'JavaSE-17',
-- path = vim.fn.expand('~/.sdkman/candidates/java/17.0.6-tem'),
-- },
-- {
-- name = 'JavaSE-18',
-- path = vim.fn.expand('~/.sdkman/candidates/java/18.0.2-amzn'),
-- },
}
cache_vars.paths = path
return path
end
local function enable_codelens(bufnr)
pcall(vim.lsp.codelens.refresh)
vim.api.nvim_create_autocmd('BufWritePost', {
buffer = bufnr,
group = java_cmds,
desc = 'refresh codelens',
callback = function()
pcall(vim.lsp.codelens.refresh)
end,
})
end
local function enable_debugger(bufnr)
require('jdtls').setup_dap({hotcodereplace = 'auto'})
require('jdtls.dap').setup_dap_main_class_configs()
local opts = {buffer = bufnr}
vim.keymap.set('n', '<leader>df', "<cmd>lua require('jdtls').test_class()<cr>", opts)
vim.keymap.set('n', '<leader>dn', "<cmd>lua require('jdtls').test_nearest_method()<cr>", opts)
end
local function jdtls_on_attach(client, bufnr)
if features.debugger then
enable_debugger(bufnr)
end
if features.codelens then
enable_codelens(bufnr)
end
-- The following mappings are based on the suggested usage of nvim-jdtls
-- https://github.com/mfussenegger/nvim-jdtls#usage
--local opts = {buffer = bufnr}
--vim.keymap.set('n', '<A-o>', "<cmd>lua require('jdtls').organize_imports()<cr>", opts)
--vim.keymap.set('n', 'crv', "<cmd>lua require('jdtls').extract_variable()<cr>", opts)
--vim.keymap.set('x', 'crv', "<esc><cmd>lua require('jdtls').extract_variable(true)<cr>", opts)
--vim.keymap.set('n', 'crc', "<cmd>lua require('jdtls').extract_constant()<cr>", opts)
--vim.keymap.set('x', 'crc', "<esc><cmd>lua require('jdtls').extract_constant(true)<cr>", opts)
--vim.keymap.set('x', 'crm', "<esc><Cmd>lua require('jdtls').extract_method(true)<cr>", opts)
end
local function jdtls_setup(event)
local jdtls = require('jdtls')
local path = get_jdtls_paths()
local data_dir = path.data_dir .. '/' .. vim.fn.fnamemodify(vim.fn.getcwd(), ':p:h:t')
if cache_vars.capabilities == nil then
jdtls.extendedClientCapabilities.resolveAdditionalTextEditsSupport = true
local ok_cmp, cmp_lsp = pcall(require, 'cmp_nvim_lsp')
cache_vars.capabilities = vim.tbl_deep_extend(
'force',
vim.lsp.protocol.make_client_capabilities(),
ok_cmp and cmp_lsp.default_capabilities() or {}
)
end
-- The command that starts the language server
-- See: https://github.com/eclipse/eclipse.jdt.ls#running-from-the-command-line
local cmd = {
-- 💀
'java',
'-Declipse.application=org.eclipse.jdt.ls.core.id1',
'-Dosgi.bundles.defaultStartLevel=4',
'-Declipse.product=org.eclipse.jdt.ls.core.product',
'-Dlog.protocol=true',
'-Dlog.level=ALL',
'-javaagent:' .. path.java_agent,
'-Xms1g',
'--add-modules=ALL-SYSTEM',
'--add-opens',
'java.base/java.util=ALL-UNNAMED',
'--add-opens',
'java.base/java.lang=ALL-UNNAMED',
-- 💀
'-jar',
path.launcher_jar,
-- 💀
'-configuration',
path.platform_config,
-- 💀
'-data',
data_dir,
}
local lsp_settings = {
java = {
-- jdt = {
-- ls = {
-- vmargs = "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx1G -Xms100m"
-- }
-- },
eclipse = {
downloadSources = true,
},
configuration = {
updateBuildConfiguration = 'interactive',
runtimes = path.runtimes,
},
maven = {
downloadSources = true,
},
implementationsCodeLens = {
enabled = true,
},
referencesCodeLens = {
enabled = true,
},
-- inlayHints = {
-- parameterNames = {
-- enabled = 'all' -- literals, all, none
-- }
-- },
format = {
enabled = true,
-- settings = {
-- profile = 'asdf'
-- },
}
},
signatureHelp = {
enabled = true,
},
completion = {
favoriteStaticMembers = {
'org.hamcrest.MatcherAssert.assertThat',
'org.hamcrest.Matchers.*',
'org.hamcrest.CoreMatchers.*',
'org.junit.jupiter.api.Assertions.*',
'java.util.Objects.requireNonNull',
'java.util.Objects.requireNonNullElse',
'org.mockito.Mockito.*',
},
},
contentProvider = {
preferred = 'fernflower',
},
extendedClientCapabilities = jdtls.extendedClientCapabilities,
sources = {
organizeImports = {
starThreshold = 9999,
staticStarThreshold = 9999,
}
},
codeGeneration = {
toString = {
template = '${object.className}{${member.name()}=${member.value}, ${otherMembers}}',
},
useBlocks = true,
},
}
-- This starts a new client & server,
-- or attaches to an existing client & server depending on the `root_dir`.
jdtls.start_or_attach({
cmd = cmd,
settings = lsp_settings,
on_attach = jdtls_on_attach,
capabilities = cache_vars.capabilities,
root_dir = jdtls.setup.find_root(root_files),
flags = {
allow_incremental_sync = true,
},
init_options = {
bundles = path.bundles,
},
})
end
vim.api.nvim_create_autocmd('FileType', {
group = java_cmds,
pattern = {'java'},
desc = 'Setup jdtls',
callback = jdtls_setup,
})
I followed this tutorial by VonKeikemen to setup JDTLS and it got LSP functionality working. The one difference is, I replaced false
with true
in
local features = {
-- change this to `true` to enable codelens
codelens = false,
-- change this to `true` if you have `nvim-dap`,
-- `java-test` and `java-debug-adapter` installed
debugger = true,
}
The last file which relates to my Java Debugging setup is my lsp.lua
file.
lsp.lua
local lsp = require('lsp-zero')
lsp.preset('recommended')
lsp.ensure_installed({
'tsserver',
'eslint',
'lua_ls',
'rust_analyzer',
'zls',
'jdtls',
})
lsp.configure('lua_ls', {
settings = {
Lua = {
diagnostics = {
globals = { 'vim' }
}
}
}
})
-- local parentDir = vim.fn.expand('%:p:h:t')
-- local filename = vim.fn.expand('%:t:r')
local cmp = require('cmp')
cmp.setup({
sources = {
{ name = 'nvim_lsp' },
{ name = 'luasnip' },
},
mapping = {
['<CR>'] = cmp.mapping.confirm({ select = false }),
},
window = {
completion = cmp.config.window.bordered(),
documentation = cmp.config.window.bordered(),
},
snippet = {
expand = function(args)
require('luasnip').lsp_expand(args.body)
end
}
})
lsp.set_preferences({
sign_icons = { }
})
lsp.on_attach(function(client, bufnr)
local opts = {buffer = bufnr, remap = false}
-- only sets keybinds for LSP after LSP has been attached
vim.keymap.set("n", "gd", function() vim.lsp.buf.definition() end, opts, { desc = 'goto definition' })
vim.keymap.set("n", "K", function() vim.lsp.buf.hover() end, opts, { desc = 'hov menu' })
vim.keymap.set("n", "<leader>vws", function() vim.lsp.buf.workspace_symbol() end, opts)
vim.keymap.set("n", "<leader>vd", function() vim.diagnostic.open_float() end, opts)
vim.keymap.set("n", "[d", function() vim.diagnostic.goto_next() end, opts)
vim.keymap.set("n", "]d", function() vim.diagnostic.goto_prev() end, opts)
vim.keymap.set("n", "<leader>vca", function() vim.lsp.buf.code_action() end, opts)
vim.keymap.set("n", "<leader>vrr", function() vim.lsp.buf.references() end, opts)
vim.keymap.set("n", "<leader>vrn", function() vim.lsp.buf.rename() end, opts)
vim.keymap.set("i", "<C-h>", function() vim.lsp.buf.signature_help() end, opts)
require('mini.clue').setup({})
if client.name == "jdt.ls" then
require("jdtls").setup_dap { hotcodereplace = "auto" }
require("jdtls.dap").setup_dap_main_class_configs()
vim.lsp.codelens.refresh()
end
end)
lsp.skip_server_setup({'jdtls'}) -- This makes the `jdtls.lua` file handle jdtls setup, rather than the LSP
lsp.setup()
Other than this, there are remaps which must be set for debugging to work
-- Debugging
vim.keymap.set("n", "<F5>", ":lua require'dap'.continue()<CR>")
vim.keymap.set("n", "<F10>", ":lua require'dap'.step_over()<CR>")
vim.keymap.set("n", "<F11>", ":lua require'dap'.step_into()<CR>")
vim.keymap.set("n", "<F12>", ":lua require'dap'.step_out()<CR>")
vim.keymap.set("n", "<leader>b", ":lua require'dap'.toggle_breakpoint()<CR>")
vim.keymap.set("n", "<leader>B", ":lua require'dap'.set_breakpoint(vim.fn.input('Breakpoint condition: '))<CR>")
vim.keymap.set("n", "<leader>lp", ":lua require'dap'.set_breakpoint(nil, nil, vim.fn.input('Log point message: '))<CR>")
vim.keymap.set("n", "<leader>dr", ":lua require'dap'.repl.open()<CR>")
The LSP servers I have installed are java-debug-adapter
, jdtls
which I installed via Mason.