Neovim as a Java IDE
Neovim has come a long way today from it’s original goal of being able to support asynchronous jobs. Today it comes packed with a ton of useful and high performance libraries and plugins. To add to that it also has a highly enthusiastic and vibrant community of lua developers who are working tirelessly to make neovim better.
Today we’ll look at how one can easily configure neovim to work similar to a modern Java IDE.
Installing Dependencies #
Ensure that you have the following dependencies installed on your system.
- Install NeoVIM, if you haven’t already. Needless to say this is a hard requirement
- Install Java (JDK), preferably version 17 or above for better compatibility with jdtls our Java Language Server
- Configure a NeoVIM to use a plugin manager. My plugin manager of choice is Lazy.nvim, however you can use any plugin manager you prefer, just refer to your plugin’s documentation for instructions on how to install and manage plugins that we’ll be using for this tutorial
Install Java Specific Plugins #
Install the Plugin none-ls for Formatting #
none-ls is a fork of null-ls that is actively maintained. (1)We install none-ls and configure it to use google-java-format as a formatter and checkstyle as a diagnostic. (2)Checkstyle is configured with an additional argument to use the google checks xml file (this is from public domain). (3)We also install mason-null-ls that helps us ensure both google-java-format and checkstyle are installed using mason
{
{
"nvimtools/none-ls.nvim",
config = function()
local nls = require("null-ls")
local fmt = nls.builtins.formatting
local dgn = nls.builtins.diagnostics
local augroup = vim.api.nvim_create_augroup("LspFormatting", {})
nls.setup({
sources = {
-- # FORMATTING #
fmt.google_java_format.with({ extra_args = { "--aosp" } }),
-- # DIAGNOSTICS #
dgn.checkstyle.with({
extra_args = {
"-c",
vim.fn.expand("~/dotfiles/config/google_checks.xml"),
},
}),
},
on_attach = function(client, bufnr)
if client.supports_method("textDocument/formatting") then
vim.api.nvim_clear_autocmds({ group = augroup, buffer = bufnr })
vim.api.nvim_create_autocmd("BufWritePre", {
group = augroup,
buffer = bufnr,
callback = function()
vim.lsp.buf.format({ bufnr = bufnr })
end,
})
end
end,
})
end,
},
{
"jay-babu/mason-null-ls.nvim",
event = { "BufReadPre", "BufNewFile" },
dependencies = {
"williamboman/mason.nvim",
"nvimtools/none-ls.nvim",
},
opt = {
ensure_installed = {
"checkstyle",
"google-java-format",
},
},
},
}
Install DAP (Debug Adapter Protocol client) #
Here we’ll install the following plugins that help us setup a working DAP client that can be used for real-time debugging.
- nvim-dap DAP Client implementation for NeoVIM
- nvim-dap-ui: A UI for DAP
- nvim-dap-virtual-text: A plugin that adds helpful virtual text for DAP
{
-- DAP
"mfussenegger/nvim-dap",
-- DAP UI
{
"rcarriga/nvim-dap-ui",
lazy = true,
dependencies = { "mfussenegger/nvim-dap", "nvim-neotest/nvim-nio" },
config = function()
require("dapui").setup()
end,
},
-- DAP Virtual Text
{
"theHamsta/nvim-dap-virtual-text",
dependencies = { "mfussenegger/nvim-dap", "nvim-treesitter/nvim-treesitter" },
config = function()
require("nvim-dap-virtual-text").setup()
end,
},
}
Install LSP Plugins #
Here we’ll install the plugins to help with configuring NeoVIM LSP configuration for Java
- mason-lspconfig.nvim: This helps us ensure our required lsp servers are installed using mason
- nvim-java: For helping automate all configurations for Java in an automated way
{
{
"williamboman/mason-lspconfig.nvim",
dependencies = { "williamboman/mason.nvim", "neovim/nvim-lspconfig" },
},
-- Setups up neovim for
-- 1. lsp (with the help of lspconfig)
-- 2. Dap configurations, etc.
-- LSP Configuration
{ "nvim-java/nvim-java" },
}
LSP Configuration #
Now that we have installed all the required plugins, we can configure LSP to work with NeoVIM. Here we’ll configure a bunch of LSP specific keybindings that are helpful in leveraging LSP functionality.
local on_attach = function(client, buffer)
vim.api.nvim_set_option_value("omnifunc", "v:lua.vim.lsp.omnifunc", { buf = bufnr })
local nmap = function(keys, func, desc)
if desc then
desc = "LSP: " .. desc
end
vim.keymap.set("n", keys, func, { buffer = bufnr, desc = desc })
end
-- Useful LSP Keymaps
nmap("gd", vim.lsp.buf.definition, "[G]oto [D]efinition")
nmap("gr", vim.lsp.buf.references, "[G]oto [R]eferences")
nmap("gI", vim.lsp.buf.implementation, "[G]oto [I]mplementation")
nmap("<leader>rn", vim.lsp.buf.rename, "[R]e[n]ame")
nmap("<leader>ca", vim.lsp.buf.code_action, "[C]ode [A]ction")
nmap("<leader>D", vim.lsp.buf.type_definition, "Type [D]efinition")
nmap("<leader>ds", require("telescope.builtin").lsp_document_symbols, "[D]ocument [S]ymbols")
nmap("<leader>ws", require("telescope.builtin").lsp_dynamic_workspace_symbols, "[W]orkspace [S]ymbols")
nmap("<leader>lr", vim.lsp.codelens.run, "[R]un [C]odelens")
nmap("<Leader>ih", function()
vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled())
end, "[I]nlay [H]ints")
-- See `:help K` for why this keymap
nmap("K", vim.lsp.buf.hover, "Hover Documentation")
nmap("<C-k>", vim.lsp.buf.signature_help, "Signature Documentation")
-- Lesser used LSP functionality
nmap("gD", vim.lsp.buf.declaration, "[G]oto [D]eclaration")
nmap("<leader>wa", vim.lsp.buf.add_workspace_folder, "[W]orkspace [A]dd Folder")
nmap("<leader>wr", vim.lsp.buf.remove_workspace_folder, "[W]orkspace [R]emove Folder")
nmap("<leader>wl", function()
print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
end, "[W]orkspace [L]ist Folders")
-- Diagnostics
nmap("gl", vim.diagnostic.open_float, "[O]pen [D]iagnostics")
nmap("[d", vim.diagnostic.goto_prev, "[G]oto [P]revious Diagnostics")
nmap("]d", vim.diagnostic.goto_next, "[G]oto [N]ext Diagnostics")
-- Enable Inalay Hints if the lsp server supports it
if client.server_capabilities.inlayHintProvider then
vim.lsp.inlay_hint.enable(true)
end
end
require("java").setup({})
require("mason").setup({})
-- List of LSP servers to be installed using mason with the help of mason-lspconfig
local servers = {
jdtls = {
settings = {
java = {
configuration = {
runtimes = {
{
name = "Java 22",
-- Set this to the path of the JDK installation
path = "<path/to/jdk>",
default = true,
}
}
}
}
}
}
}
local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities = require("cmp_nvim_lsp").default_capabilities(capabilities)
local mason_lspconfig = require("mason-lspconfig")
mason_lspconfig.setup({
ensure_installed = vim.tbl_keys(servers),
})
mason_lspconfig.setup_handlers({
function(server_name)
require("lspconfig")[server_name].setup({
capabilities = capabilities,
on_attach = on_attach,
settings = servers[server_name],
})
end,
})
DAP Configuration #
Now that LSP has been configured, lets configure DAP as well.
local dap, dapui = require("dap"), require("dapui")
-- Auto Open DAP UI when debugging starts
dap.listeners.after.event_initialized["dapui_config"] = function()
dapui.open()
end
-- Auto Close DAP UI when debugging ends
dap.listeners.before.event_terminated["dapui_config"] = function()
dapui.close()
end
dap.listeners.after.event_exited["dapui_config"] = function()
dapui.close()
end
-- Useful DAP Keymaps
vim.keymap.set("n", "<Leader>do", function()
require("dapui").open()
end, { desc = "dapui.open" })
vim.keymap.set("n", "<Leader>dc", function()
require("dap").continue()
end, { desc = "dap.continue" })
vim.keymap.set("n", "<Leader>dso", function()
require("dap").step_over()
end, { desc = "dap.step_over" })
vim.keymap.set("n", "<Leader>dsi", function()
require("dap").step_into()
end, { desc = "dap.step_into" })
vim.keymap.set("n", "<Leader>dsb", function()
require("dap").step_out()
end, { desc = "dap.step_out" })
vim.keymap.set("n", "<Leader>b", function()
require("dap").toggle_breakpoint()
end, { desc = "dap.toggle_breakpoint" })
vim.keymap.set("n", "<Leader>B", function()
require("dap").set_breakpoint(vim.fn.input("Breakpoint condition: "))
end, { desc = "dap.set_breakpoint with condition" })
vim.keymap.set("n", "<Leader>lp", function()
require("dap").set_breakpoint(nil, nil, vim.fn.input("Log point message: "))
end, { desc = "dap.set_breakpoint with log point message" })
vim.keymap.set("n", "<Leader>dr", function()
require("dap").repl.open()
end, { desc = "dap.repl.open" })
vim.keymap.set("n", "<Leader>dl", function()
require("dap").run_last()
end, { desc = "dap.run_last" })
vim.keymap.set("n", "<Leader>dq", function()
require("dapui").close()
end, { desc = "dapui.close" })
vim.keymap.set({ "n", "v" }, "<Leader>dh", function()
require("dap.ui.widgets").hover()
end, { desc = "dap.ui.widgets.hover" })
vim.keymap.set({ "n", "v" }, "<Leader>dp", function()
require("dap.ui.widgets").preview()
end, { desc = "dap.ui.widgets.preview" })
vim.keymap.set("n", "<Leader>df", function()
local widgets = require("dap.ui.widgets")
widgets.centered_float(widgets.frames)
end, { desc = "dap.ui.widgets.frames" })
vim.keymap.set("n", "<Leader>dsc", function()
local widgets = require("dap.ui.widgets")
widgets.centered_float(widgets.scopes)
end, { desc = "dap.ui.widgets.scopes" })
If you want to debug remotely or by running the debug server within docker, you can use the VS Code
launch.json
file to configure the debug server.You can load vscode launch.json file which is typically found ta
./.vscode/launch.json
using this :require("dap.ext.vscode").load_launchjs()
This is how your launch.json would look like :
{ "version": "0.2.0", "configurations": [ { "name": "Debug Project", "type": "java", "request": "attach", "hostName": "127.0.0.1", "port": 8000, "projectName": "<project-name>", "sourcePaths": ["src"], "mainClass": "com.package.MainClass" } ] }
Conclusion #
With this setup, Neovim now provides a robust environment for Java development, including syntax highlighting, LSP support, autocompletion, and debugging capabilities. This setup ensures that you have a streamlined and efficient workflow for Java development.
If you have any questions or need further clarifications, feel free to ask.