feat: add Java support with jdtls, DAP, neotest and google-java-format

- Add nvim-jdtls for Java LSP with Maven/Gradle project detection
- Create ftplugin/java.lua for automatic jdtls startup and keymaps
- Enable nvim-dap with java-debug-adapter for breakpoint debugging
- Add neotest with neotest-java adapter for JUnit 5 test runner
- Add NeotestJavaDownload command for JUnit jar setup on Neovim < 0.12
- Configure google-java-format via conform.nvim
- Add java treesitter parser and <leader>j which-key group
- Mason auto-installs jdtls, java-debug-adapter, java-test, google-java-format
This commit is contained in:
Maxime Pierront 2026-02-14 15:09:55 +01:00
parent 1a776b3be1
commit 6e454444fd
3 changed files with 176 additions and 2 deletions

121
ftplugin/java.lua Normal file
View File

@ -0,0 +1,121 @@
-- ftplugin/java.lua
-- This file is automatically loaded when a Java file is opened.
-- It configures jdtls (Eclipse JDT Language Server) for Java development.
local jdtls = require 'jdtls'
-- Mason install paths
local mason_path = vim.fn.stdpath 'data' .. '/mason/packages'
local jdtls_path = mason_path .. '/jdtls'
local java_debug_path = mason_path .. '/java-debug-adapter'
local java_test_path = mason_path .. '/java-test'
-- Detect the OS for the jdtls config directory
local os_config = 'config_linux'
if vim.fn.has 'mac' == 1 then
os_config = 'config_mac'
elseif vim.fn.has 'win32' == 1 then
os_config = 'config_win'
end
-- Find the launcher jar
local launcher_jar = vim.fn.glob(jdtls_path .. '/plugins/org.eclipse.equinox.launcher_*.jar')
-- Determine the project name for workspace isolation
local project_name = vim.fn.fnamemodify(vim.fn.getcwd(), ':p:h:t')
local workspace_dir = vim.fn.stdpath 'data' .. '/jdtls-workspace/' .. project_name
-- Find the root directory of the project
local root_dir = require('jdtls.setup').find_root { 'pom.xml', 'build.gradle', 'gradlew', '.git', 'mvnw' }
-- Collect debug and test bundles
local bundles = {}
-- java-debug-adapter
local debug_jar = vim.fn.glob(java_debug_path .. '/extension/server/com.microsoft.java.debug.plugin-*.jar', true)
if debug_jar ~= '' then
table.insert(bundles, debug_jar)
end
-- java-test
local test_jars = vim.split(vim.fn.glob(java_test_path .. '/extension/server/*.jar', true), '\n')
for _, jar in ipairs(test_jars) do
if jar ~= '' then
table.insert(bundles, jar)
end
end
-- jdtls configuration
local config = {
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',
'-Xmx1g',
'--add-modules=ALL-SYSTEM',
'--add-opens', 'java.base/java.util=ALL-UNNAMED',
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
'-jar', launcher_jar,
'-configuration', jdtls_path .. '/' .. os_config,
'-data', workspace_dir,
},
root_dir = root_dir,
settings = {
java = {
signatureHelp = { enabled = true },
contentProvider = { preferred = 'fernflower' },
completion = {
favoriteStaticMembers = {
'org.junit.jupiter.api.Assertions.*',
'org.mockito.Mockito.*',
'org.mockito.ArgumentMatchers.*',
'java.util.Objects.requireNonNull',
'java.util.Objects.requireNonNullElse',
},
},
sources = {
organizeImports = {
starThreshold = 9999,
staticStarThreshold = 9999,
},
},
},
},
init_options = {
bundles = bundles,
},
-- Called when jdtls attaches to a buffer
on_attach = function(_, bufnr)
-- Enable debug and test support after LSP is ready
jdtls.setup_dap { hotcodereplace = 'auto' }
require('jdtls.dap').setup_dap_main_class_configs()
local opts = { buffer = bufnr }
-- Java-specific keymaps under <leader>j
vim.keymap.set('n', '<leader>ji', jdtls.organize_imports, vim.tbl_extend('force', opts, { desc = '[J]ava Organize [I]mports' }))
vim.keymap.set('n', '<leader>jc', jdtls.extract_constant, vim.tbl_extend('force', opts, { desc = '[J]ava Extract [C]onstant' }))
vim.keymap.set('v', '<leader>jm', function() jdtls.extract_method(true) end, vim.tbl_extend('force', opts, { desc = '[J]ava Extract [M]ethod' }))
vim.keymap.set('v', '<leader>jv', jdtls.extract_variable, vim.tbl_extend('force', opts, { desc = '[J]ava Extract [V]ariable' }))
-- Test keymaps using neotest
local neotest = require 'neotest'
vim.keymap.set('n', '<leader>jt', function() neotest.run.run() end, vim.tbl_extend('force', opts, { desc = '[J]ava Run [T]est (cursor)' }))
vim.keymap.set('n', '<leader>jf', function() neotest.run.run(vim.fn.expand '%') end, vim.tbl_extend('force', opts, { desc = '[J]ava Run Tests [F]ile' }))
vim.keymap.set('n', '<leader>js', function() neotest.summary.toggle() end, vim.tbl_extend('force', opts, { desc = '[J]ava Test [S]ummary' }))
vim.keymap.set('n', '<leader>jo', function() neotest.output.open { enter = true } end, vim.tbl_extend('force', opts, { desc = '[J]ava Test [O]utput' }))
-- Debug test under cursor
vim.keymap.set('n', '<leader>jd', function() neotest.run.run { strategy = 'dap' } end, vim.tbl_extend('force', opts, { desc = '[J]ava [D]ebug Test' }))
end,
}
-- Start or attach jdtls
jdtls.start_or_attach(config)

View File

@ -334,6 +334,7 @@ require('lazy').setup({
{ '<leader>h', group = 'Git [H]unk', mode = { 'n', 'v' } }, { '<leader>h', group = 'Git [H]unk', mode = { 'n', 'v' } },
{ '<leader>g', group = '[G]it' }, { '<leader>g', group = '[G]it' },
{ '<leader>gf', group = '[F]ind' }, { '<leader>gf', group = '[F]ind' },
{ '<leader>j', group = '[J]ava' },
}, },
}, },
}, },
@ -672,6 +673,10 @@ require('lazy').setup({
'vue-language-server', -- Vue.js LSP (Volar) 'vue-language-server', -- Vue.js LSP (Volar)
'eslint-lsp', -- ESLint LSP 'eslint-lsp', -- ESLint LSP
'prettierd', -- Prettier daemon (formatter) 'prettierd', -- Prettier daemon (formatter)
'jdtls', -- Java LSP (Eclipse JDT)
'java-debug-adapter', -- Java debug adapter
'java-test', -- Java test runner
'google-java-format', -- Java formatter
} }
require('mason-tool-installer').setup { ensure_installed = ensure_installed } require('mason-tool-installer').setup { ensure_installed = ensure_installed }
@ -749,6 +754,7 @@ require('lazy').setup({
css = { 'prettierd', 'prettier', stop_after_first = true }, css = { 'prettierd', 'prettier', stop_after_first = true },
html = { 'prettierd', 'prettier', stop_after_first = true }, html = { 'prettierd', 'prettier', stop_after_first = true },
json = { 'prettierd', 'prettier', stop_after_first = true }, json = { 'prettierd', 'prettier', stop_after_first = true },
java = { 'google-java-format' },
}, },
}, },
}, },
@ -913,13 +919,59 @@ require('lazy').setup({
opts = {}, opts = {},
}, },
{ -- Java LSP support via jdtls
'mfussenegger/nvim-jdtls',
ft = 'java',
},
{ -- Test runner framework
'nvim-neotest/neotest',
dependencies = {
'nvim-neotest/nvim-nio',
'nvim-lua/plenary.nvim',
'nvim-treesitter/nvim-treesitter',
'rcasia/neotest-java', -- JUnit 5 adapter
},
config = function()
require('neotest').setup {
adapters = {
require 'neotest-java',
},
}
-- Command to download JUnit jar on machines without Neovim 0.12+
vim.api.nvim_create_user_command('NeotestJavaDownload', function()
local version = '1.10.1'
local dir = vim.fn.stdpath 'data' .. '/neotest-java'
local jar = dir .. '/junit-platform-console-standalone-' .. version .. '.jar'
if vim.fn.filereadable(jar) == 1 then
vim.notify('JUnit jar already exists at ' .. jar, vim.log.levels.INFO)
return
end
vim.fn.mkdir(dir, 'p')
local url = 'https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/'
.. version
.. '/junit-platform-console-standalone-'
.. version
.. '.jar'
vim.notify('Downloading JUnit Platform Console Standalone ' .. version .. '...', vim.log.levels.INFO)
vim.fn.system { 'curl', '-L', '-o', jar, url }
if vim.v.shell_error == 0 then
vim.notify('JUnit jar downloaded successfully!', vim.log.levels.INFO)
else
vim.notify('Failed to download JUnit jar', vim.log.levels.ERROR)
end
end, { desc = 'Download JUnit Platform Console Standalone jar for neotest-java' })
end,
},
{ -- Highlight, edit, and navigate code { -- Highlight, edit, and navigate code
'nvim-treesitter/nvim-treesitter', 'nvim-treesitter/nvim-treesitter',
build = ':TSUpdate', build = ':TSUpdate',
config = function() config = function()
---@diagnostic disable-next-line: missing-fields ---@diagnostic disable-next-line: missing-fields
require('nvim-treesitter.configs').setup { require('nvim-treesitter.configs').setup {
ensure_installed = { 'bash', 'c', 'css', 'diff', 'html', 'javascript', 'json', 'lua', 'luadoc', 'markdown', 'markdown_inline', 'query', 'tsx', 'typescript', 'vim', 'vimdoc', 'vue' }, ensure_installed = { 'bash', 'c', 'css', 'diff', 'html', 'java', 'javascript', 'json', 'lua', 'luadoc', 'markdown', 'markdown_inline', 'query', 'tsx', 'typescript', 'vim', 'vimdoc', 'vue' },
auto_install = true, auto_install = true,
highlight = { enable = true }, highlight = { enable = true },
indent = { enable = true }, indent = { enable = true },
@ -936,7 +988,7 @@ require('lazy').setup({
-- Here are some example plugins that I've included in the Kickstart repository. -- Here are some example plugins that I've included in the Kickstart repository.
-- Uncomment any of the lines below to enable them (you will need to restart nvim). -- Uncomment any of the lines below to enable them (you will need to restart nvim).
-- --
-- require 'kickstart.plugins.debug', require 'kickstart.plugins.debug',
-- require 'kickstart.plugins.indent_line', -- require 'kickstart.plugins.indent_line',
-- require 'kickstart.plugins.lint', -- require 'kickstart.plugins.lint',
-- require 'kickstart.plugins.autopairs', -- require 'kickstart.plugins.autopairs',

View File

@ -81,6 +81,7 @@ return {
ensure_installed = { ensure_installed = {
-- Update this to ensure that you have the debuggers for the langs you want -- Update this to ensure that you have the debuggers for the langs you want
'delve', 'delve',
'java-debug-adapter',
}, },
} }