From 6e454444fd4a020610b036359a0427ddf08c47c8 Mon Sep 17 00:00:00 2001 From: Maxime Pierront Date: Sat, 14 Feb 2026 15:09:55 +0100 Subject: [PATCH] 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 j which-key group - Mason auto-installs jdtls, java-debug-adapter, java-test, google-java-format --- ftplugin/java.lua | 121 ++++++++++++++++++++++++++++++++ init.lua | 56 ++++++++++++++- lua/kickstart/plugins/debug.lua | 1 + 3 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 ftplugin/java.lua diff --git a/ftplugin/java.lua b/ftplugin/java.lua new file mode 100644 index 00000000..aafb2b6f --- /dev/null +++ b/ftplugin/java.lua @@ -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 j + vim.keymap.set('n', 'ji', jdtls.organize_imports, vim.tbl_extend('force', opts, { desc = '[J]ava Organize [I]mports' })) + vim.keymap.set('n', 'jc', jdtls.extract_constant, vim.tbl_extend('force', opts, { desc = '[J]ava Extract [C]onstant' })) + vim.keymap.set('v', 'jm', function() jdtls.extract_method(true) end, vim.tbl_extend('force', opts, { desc = '[J]ava Extract [M]ethod' })) + vim.keymap.set('v', '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', 'jt', function() neotest.run.run() end, vim.tbl_extend('force', opts, { desc = '[J]ava Run [T]est (cursor)' })) + vim.keymap.set('n', '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', 'js', function() neotest.summary.toggle() end, vim.tbl_extend('force', opts, { desc = '[J]ava Test [S]ummary' })) + vim.keymap.set('n', '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', '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) diff --git a/init.lua b/init.lua index ba6092fd..ccad5da5 100644 --- a/init.lua +++ b/init.lua @@ -334,6 +334,7 @@ require('lazy').setup({ { 'h', group = 'Git [H]unk', mode = { 'n', 'v' } }, { 'g', group = '[G]it' }, { 'gf', group = '[F]ind' }, + { 'j', group = '[J]ava' }, }, }, }, @@ -672,6 +673,10 @@ require('lazy').setup({ 'vue-language-server', -- Vue.js LSP (Volar) 'eslint-lsp', -- ESLint LSP '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 } @@ -749,6 +754,7 @@ require('lazy').setup({ css = { 'prettierd', 'prettier', stop_after_first = true }, html = { '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 = {}, }, + { -- 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 'nvim-treesitter/nvim-treesitter', build = ':TSUpdate', config = function() ---@diagnostic disable-next-line: missing-fields 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, highlight = { 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. -- 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.lint', -- require 'kickstart.plugins.autopairs', diff --git a/lua/kickstart/plugins/debug.lua b/lua/kickstart/plugins/debug.lua index 1e3570f9..a79dfa9d 100644 --- a/lua/kickstart/plugins/debug.lua +++ b/lua/kickstart/plugins/debug.lua @@ -81,6 +81,7 @@ return { ensure_installed = { -- Update this to ensure that you have the debuggers for the langs you want 'delve', + 'java-debug-adapter', }, }