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 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.

{
    -- 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

{
    {
      "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.

comments powered by Disqus