Last updated: Wed Feb 21 2024
A step by step guide to set up Neovim for Java development, capable of fully replacing IntelliJ. This is not a copy-paste solution but rather a series of instructions that you can adapt to your existing configuration. I have confirmed that it works for Linux and MacOS, and it may work for Windows too (with some minor tweaks).
You can find the minimal setup in this GitHub repository, I'll try to keep it up to date, but feel free to open an issue if I miss something.
Mason.nvim allows you to manage
third party dependencies such as Language Servers, linters, and other binaries
that you will require through this guide. Follow the
installation instructions
and call the setup()
function.
-- Plugin manager: add mason.nvim
{ 'williamboman/mason.nvim' }
-- init.lua
require'mason'.setup()
Eclipse's JDT Language Server
provides the language comprehension required to be notified of syntax errors,
navigate through the code, autocompletion (fully set up in
step 3), and code actions such as add missing imports.
You can install the language server with Mason by running :MasonInstall jdtls
,
you will also need the nvim-jdtls
plugin that is the Neovim client for that server.
-- Plugin manager: add nvim-jdtls
{ 'mfussenegger/nvim-jdtls' }
To set it up you need to call start_or_attach
when a Java file is open,
this can be achieved with an autocmd or java.lua
file inside ftplugin/
directory. You need to populate the cmd property with the jdtls executable.
If you've installed it using Mason, an executable script should be located in
$HOME/.local/share/nvim/mason/bin/jdtls
.
-- ftplugin/java.lua: call start_or_attach when a Java file is loaded
require'jdtls'.start_or_attach({
cmd = {
vim.fn.expand'$HOME/.local/share/nvim/mason/bin/jdtls',
}
})
You should have a working client and server, open any Java project to see the
Language Server loading. It may take a while but you should be able to see
information about unused variables and syntax errors. There are also new
commands available inside a Java file, you can type :Jdt
and autocomplete the
options.
Note: Python3 and Java 17+ is required for jdtls to work. If you run
into any issues try executing the binary directly from the command line
(I already gave you the path) or call :JdtShowLogs
to get client logs.
Add the Java agent to JDTLS with the flag -javaagent
pointing to the location
of lombok.jar
. If you used Mason.nvim, it already came with your JDTLS
instalation. Update nvim-jdtls setup like this:
-- ftplugin/java.lua: add arguments to jdtls script
require'jdtls'.start_or_attach({
cmd = {
vim.fn.expand'$HOME/.local/share/nvim/mason/bin/jdtls',
+ ('--jvm-arg=-javaagent:%s'):format(vim.fn.expand'$HOME/.local/share/nvim/mason/packages/jdtls/lombok.jar')
}
})
The most popular completion plugin is nvim-cmp which has a couple dependencies, make sure to install them all including the source for LSP completion.
-- Plugin manager: install nvim-cmp, LuaSnip, cmp_luasnip, and cmp-nvim-lsp
{
'hrsh7th/nvim-cmp',
version = false, -- Ignore tags because nvim-cmp has a very old tag
dependencies = {
'L3MON4D3/LuaSnip', -- Snippet engine
'saadparwaiz1/cmp_luasnip', -- Snippet engine adapter
'hrsh7th/cmp-nvim-lsp', -- Source for LSP completion
},
}
-- Plugin manager: add cmp-nvim-lsp as dependency to nvim-jdtls
{
'mfussenegger/nvim-jdtls',
+ dependencies = 'hrsh7th/cmp-nvim-lsp',
},
Next step is to set up nvim-cmp by calling setup()
to configure the snippet engine,
essential mappings, and the LSP completion source.
-- init.lua: setup nvim-cmp
require'cmp'.setup({
snippet = {
-- Exclusive to LuaSnip, check nvim-cmp documentation for usage with a different snippet engine
expand = function(args)
require'luasnip'.lsp_expand(args.body)
end
},
mapping = {
-- Sample but necessary mappings, read nvim-cmp documentation to customize them
['<C-c>'] = require'cmp'.mapping.abort(),
['<CR>'] = require'cmp'.mapping.confirm(),
['<C-n>'] = require'cmp'.mapping.select_next_item(),
['<C-p>'] = require'cmp'.mapping.select_prev_item(),
},
sources = {
{ name = 'nvim_lsp' },
},
})
Finally, connect nvim-jdtls with nvim-cmp by adding completion capabilities.
-- ftplugin/java.lua
require'jdtls'.start_or_attach{
cmd = {
vim.fn.expand'$HOME/.local/share/nvim/mason/bin/jdtls',
('--jvm-arg=-javaagent:%s'):format(vim.fn.expand'$HOME/.local/share/nvim/mason/packages/jdtls/lombok.jar')
},
+ capabilities = require'cmp_nvim_lsp'.default_capabilities()
}
You should have completion working, open a Java file and start typing. You can
cycle through the results with <C-n>
and <C-P>
, and select them with <CR>
.
Neotest provides a great interface for running tests, for it to work you need to install it alongside its dependencies including nvim-treesitter.
-- Plugin manager: install neotest with its necessary dependencies
{
'nvim-neotest/neotest',
dependencies = {
'nvim-lua/plenary.nvim',
'nvim-treesitter/nvim-treesitter',
'antoinemadec/FixCursorHold.nvim',
'rcasia/neotest-java',
},
}
-- init.lua: setup neotest
require'neotest'.setup({
adapters = {
require'neotest-java',
},
})
After installing neotest and its dependencies you are going to need the Java
parser, install it by calling :TSInstall java
(note: a C compiler is
required to build the parser, you can use GCC or Clang).
You should be able to invoke the following commands to view and run tests.
If you run into any trouble you can check neotest logs in ~/.local/state/nvim/neotest.log
require('neotest').output_panel.toggle() -- Opens/closes test pannel
require('neotest').summary.toggle() -- Toggle summary
require('neotest').run.run() -- Test nearest
require('neotest').run.run(vim.fn.expand('%')) -- Test file
require('neotest').run.run(vim.loop.cwd()) -- Test project
require('neotest').run.stop() -- Stop testing
To run and debug code you need a combination of nvim-jdtls, nvim-dap, and nvim-dap-ui. We also need to install java-debug.
Install the necessary plugins, you should already have nvim-jdtls from
step 2 so you only need to add nvim-dap
and its ui. There's no need to call setup()
here.
-- Plugin manager: add nvim-dap-ui and nvim-dap
{
'rcarriga/nvim-dap-ui',
dependencies = 'mfussenegger/nvim-dap',
},
Install the debug server with :MasonInstall java-debug-adapter
, and
bundle the jar together with nvim-djtls by adding it to the bundles
property. This property takes a list of paths to jar files.
require'jdtls'.start_or_attach({
cmd = {
vim.fn.expand'$HOME/.local/share/nvim/mason/bin/jdtls',
('--jvm-arg=-javaagent:%s'):format(vim.fn.expand'$HOME/.local/share/nvim/mason/packages/jdtls/lombok.jar'),
},
capabilities = require'cmp_nvim_lsp'.default_capabilities(),
+ bundles = { vim.fn.expand'$HOME/.local/share/nvim/mason/packages/java-debug-adapter/extension/server/com.microsoft.java.debug.plugin-*.jar' },
})
You should be able to open any Java project, run :JdtUpdateDebugConfigs
,
and access the following commands:
require'dap'.toggle_breakpoint() -- Set or unset breakpoint
require'dap'.continue() -- Start debuging or continue to next breakpoint
require'dap'.step_over() -- Step over
require'dap'.step_into() -- Step into
require'dap'.repl.open() -- Open repl
require'dap'.restart() -- Restart debugging session
require'dap'.close() -- Close debugging session
require'dapui'.eval() -- See runtime values of the variables under cursor
require'dapui'.toggle() -- Open or close debugging UI
While we wait for neotest to support debugging we can rely on nvim-dap to debug tests.
A working language server and debug adapter
are required. start by installing java-test
with mason :MasonInstall java-test
, and update your jdtls configuration to include
a list of all jars from both java-debug and java-test. Because Mason installs
them in similar paths you can use vim.fn.glob
function to get a newline separated
string containing all required jars that you can convert into a list usin vim.split
.
we can use vim.fn.glob
to get a newline separated string containing al jars,
require'jdtls'.start_or_attach({
cmd = {
vim.fn.expand'$HOME/.local/share/nvim/mason/bin/jdtls',
('--jvm-arg=-javaagent:%s'):format(vim.fn.expand'$HOME/.local/share/nvim/mason/packages/jdtls/lombok.jar'),
},
capabilities = require'cmp_nvim_lsp'.default_capabilities(),
- bundles = { vim.fn.expand'$HOME/.local/share/nvim/mason/packages/java-debug-adapter/extension/server/com.microsoft.java.debug.plugin-*.jar' },
+ bundles = vim.split(vim.fn.glob('$HOME/.local/share/nvim/mason/packages/java-*/extension/server/*.jar', 1), '\n'),
})
You should have access to two new functions that will help you debug tests, together with the previously mentioned commands you should be able to set breakpoints and debug normally.
require'jdtls'.test_class() -- Run all tests in class
require'jdtls'.test_nearest_method() -- Run test closest to cursor
I hope this helps you set up your environment. I have made this guide to celebrate my javaniversary as today (February 17, 2024) is my third year developing Java on a daily basis, and I have been using Neovim for Java development since aproximately that much minus 2 months that took me to figure out how to set it up.
Additional resources:
Plugins used:
Packages used: