Brewing Up a Java Setup For Neovim
Table of Contents
I love neovim. I’ve used it happily for Python, JavaScript, and other languages, but when I started working on Java projects, I discovered a neovim niche. The overlap between Java devs and neovim enthusiasts is apparently pretty slim. There weren’t nearly as many resources for configuring a solid Java setup as there are for Python, Go, or TypeScript. Instead, I found one too many unsatisfying Reddit posts where someone asked how to set up Java in neovim and most answers boiled down to “I tried, but just ended up going back to IntelliJ.” It took some time, but I’ve got a functional setup now. It’s not perfect, but I’ve been using it for full-time Java development for months, with only attaching a debugger being a subpar experience. Here’s what I wish I’d had when I started.
Installing and Configuring a Java LSP #
Pretty much the only viable Java language server option is eclipse.jdt.ls paired with a neovim client. It is in wide-spread use, notably as the backing LSP server for the VS Code Java extension pack.
The following sections describe how to get this setup in neovim, including a couple of important gotchas.
Installing eclipse.jdt.ls #
jdtls gives you instructions to download the server and a python script to run it, but I just had Mason.nvim handle that. I also added lemminx, an LSP for XML files (hello, Maven).
Here’s my mason.lua:
-- lua/plugins/mason.lua
return {
{
"mason-org/mason.nvim",
opts = {
ensure_installed = {
"jdtls",
"lemminx",
},
},
},
}
Setting Up nvim-jdtls #
nvim-jdtls is the neovim client I chose for the jdtls language server. There is also a coc.nvim plugin option, but since I don’t use coc.nvim for anything else, I went with the standalone option.
nvim-jdtls (really eclipse.jdt.ls) provides a lot of tools and config out of the box, but it didn’t work as expected without some tuning. It was a bit of a journey to get things working well, but in the end it turned out to be pretty simple.
I only made two customizations to jdtls, but they were make or break for this setup.
Customization 1: Hiding Eclipse Metadata Files #
Without this flag, jdtls rudely spews .settings and .project files all over your project directory. Setting generatesMetadataFilesAtProjectRoot=false keeps the Eclipse metadata out of your repo.
The real trick is the way you pass jvm args to jdtls to configure its behavior. I found no resource that helped me figure this out so I hope this finds someone who has been banging their head against a wall trying to get this working.
After much annoyance and frustration on my part, I present you one beautiful line.
table.insert(opts.cmd, "--jvm-arg=-Djava.import.generatesMetadataFilesAtProjectRoot=false")
You need both --jvm-arg and -D to get jdtls to care. Full plugin config is farther below.
Customization 2: Increase Language Server Memory For Big Projects #
Once I got the eclipse metadata problem figured out I started using the language server and found that jdtls frequently choked when running on larger java projects. This one was solved in the regular java way, but this time with no --jvm-arg flag required.
table.insert(opts.cmd, "-Xmx8G")
Not sure I needed 8Gb of memory for a language server, but I was aging visibly waiting for the LSP to unfreeze.
Full nvim-jdtls Plugin Spec #
The plugin spec goes in lua/plugins/java.lua. The first block configures nvim-jdtls itself:
-- lua/plugins/java.lua (nvim-jdtls config)
{
"mfussenegger/nvim-jdtls",
opts = function(_, opts)
-- prevent .settings, .project, etc files from being generated in the project folder
table.insert(opts.cmd, "--jvm-arg=-Djava.import.generatesMetadataFilesAtProjectRoot=false")
table.insert(opts.cmd, "-Xmx8G")
opts.settings = {
java = {
format = {
enabled = true,
comments = { enabled = false },
tabSize = 4,
},
},
}
end,
},
XML and Maven Support with Lemminx #
Since doing more java development, I have regretfully had to start reading and editing a lot more xml. While I do love manually typing and matching XML tags like I’m transcribing a medieval manuscript, Lemminx keeps the suffering to a dull roar.
Lemminx gives you completions, validation, and formatting on XML files.
Here’s the lspconfig block from the same java.lua:
-- lua/plugins/java.lua (lemminx config)
{
"neovim/nvim-lspconfig",
---@class PluginLspOpts
opts = {
servers = {
lemminx = {
init_options = {
settings = {
xml = {
format = {
enabled = true,
splitAttributes = "preserve",
maxLineWidth = 280,
},
},
xslt = {
format = {
enabled = true,
splitAttributes = "preserve",
maxLineWidth = 280,
},
},
},
},
},
},
},
},
The key settings: splitAttributes: preserve keeps your XML attributes where you put them instead of reformatting them, and the wide maxLineWidth prevents Lemminx from wrapping long dependency declarations into unreadable chunks.
Code Formatting #
Code formatting isn’t ideal. I use the Maven Spotless plugin in most projects with fairly sophisticated rules. I tried invoking Spotless on postWrite hooks in Neovim, but that was a fragile implementation that broke more than it worked.
Instead, I rely on jdtls’s built-in formatter to keep the code from looking utterly atrocious as it’s written, then manually run mvn spotless:apply before I commit. I have jdtls set to a tabSize of 4 with comment formatting disabled (I like my comments the way I write them, thanks), and a matching ftplugin to keep Neovim’s tabs consistent:
-- ftplugin/java.lua
vim.opt_local.tabstop = 4
vim.opt_local.shiftwidth = 4
What’s Not Working Well #
- I would prefer a more integrated code formatting experience using Spotless, but
mvn spotless:applyworks well enough that I don’t care to find a better solution. - Debugging using nvim-dap works, but it’s been (ironically) a bit buggy. I usually revert to VS Code if I need to spend significant time debugging something.
Wrapping Up #
Looking back, it seems laughably easy to set this up, but as always the devil’s in the details. Missing that --jvm-arg flag was roughly 95% of the problem.
I didn’t have a great resource for setting up java with neovim, so I hope this post will at least point others in a better direction.
If you want to see my full config, it lives in my neovim-config repo. The relevant bits are in lua/plugins/java.lua, lua/plugins/mason.lua, and ftplugin/java.lua.