Status: experimental (v0.1.x) — actively used by the author on macOS, partially tested on Linux/Windows. Feedback and issues welcome.
Auto-detect installed JDKs by major version and inject their paths into Neovim env vars. Configure separate JDKs for jdtls (the language server) and Gradle (the build tool).
require("jvm-env").setup({ jdtls = "21", gradle = "17" })
-- → vim.env.JDTLS_JAVA_HOME = <JDK 21 home>
-- → vim.env.GRADLE_JAVA_HOME = <JDK 17 home>
jenv prefix / /usr/libexec/java_home / ~/.sdkman / /usr/lib/jvm/* / standard Windows JDK paths / scoop per OS, in order to locate the JDK home for the requested major version.vim.env.JDTLS_JAVA_HOME and vim.env.GRADLE_JAVA_HOME.vim.notify and keeps going — never blocks other startup.:checkhealth jvm-env to see which managers are visible and which paths each version resolves to.JAVA_HOME. Your shell's JAVA_HOME is left alone; jvm-env uses two dedicated variables instead, deliberately avoiding conflicts with other tools.A common Java setup:
Toggling shell JAVA_HOME for every project is awkward, and the nvim-jdtls README's hard-coded cmd = { '/usr/lib/jvm/...' } does not survive cross-platform / multi-version setups. jvm-env is a thin helper that takes a major-version string and resolves the right path for the current OS.
| Tool | Scope |
|---|---|
| mason.nvim | Installs LSP servers like jdtls. Does not install JDKs. |
| nvim-jdtls | jdtls integration for Neovim. JDK paths are your responsibility. |
| nvim-java | Full stack (LSP + DAP + tests). Can install JDKs via mason. |
| LazyVim java extras | Auto-wires jdtls. Recommends a hard-coded vim.env.JAVA_HOME = .... |
| jvm-env (this plugin) | OS/manager detection + split jdtls/Gradle env vars. No LSP. |
These are complementary. If you already use a full-stack solution like nvim-java, you don't need this. jvm-env is for users layering light automation on top of nvim-jdtls or LazyVim Java extras.
The "auto-detect JDK by major version and inject into env vars" niche is not covered by the above — see nvim-jdtls discussion #548 and mason.nvim discussion #1893, where the answer is consistently "set JAVA_HOME yourself." jvm-env fills that gap.
vim.uv and the new vim.health API).{
"clang-engineer/jvm-env.nvim",
lazy = false, -- vim.env must be set before jdtls starts
priority = 100, -- load before LSP/jdtls related plugins
opts = {
jdtls = "21",
gradle = "17",
},
}
Leave opts empty to keep the defaults (jdtls = "21", gradle = "17").
require("jvm-env").setup()
-- fills both env vars with the defaults (jdtls=21, gradle=17)
require("jvm-env").setup({ jdtls = "21", gradle = "17" })
Pass false to skip detection for that env var entirely (no warning, no value set):
require("jvm-env").setup({ jdtls = "21", gradle = false })
-- only JDTLS_JAVA_HOME is touched
lua/plugins/java.lua:
return {
{
"mfussenegger/nvim-jdtls",
opts = function(_, opts)
local function present(v) return v ~= nil and v ~= "" end
-- jvm-env (loaded eagerly above) has already populated these.
local jdtls_home = vim.env.JDTLS_JAVA_HOME
local gradle_home = vim.env.GRADLE_JAVA_HOME
if not present(jdtls_home) then return opts end
local cmd = { vim.fn.exepath("jdtls") }
table.insert(cmd, "--java-executable")
table.insert(cmd, jdtls_home .. "/bin/java")
opts.jdtls = vim.tbl_deep_extend("force", opts.jdtls or {}, {
cmd = cmd,
cmd_env = present(gradle_home) and {
JAVA_HOME = gradle_home,
GRADLE_OPTS = "-Dorg.gradle.java.home=" .. gradle_home,
} or nil,
})
end,
},
}
Key points:
cmd uses --java-executable to pin the JDK that runs jdtls → reads JDTLS_JAVA_HOME.cmd_env.JAVA_HOME isolates the JDK used by the Gradle process spawned by jdtls → reads GRADLE_JAVA_HOME..nvim.lua.nvim.lua (loaded per directory via Neovim 0.9+ exrc):
require("jvm-env").setup({ jdtls = "21", gradle = "17" })
Or run :JvmEnvInit 21 17 (versions optional — falls back to the active config) in the project root to write that line for you.
Enable with vim.o.exrc = true and :trust the file once. Reopening Neovim inside that directory switches to the project-specific versions automatically.
| OS | Order |
|---|---|
| macOS | 1. jenv prefix <ver> (if jenv is installed) → 2. jenv versions --bare filtered to <ver>.* (exact fallback) → 3. /usr/libexec/java_home -v <ver> → 4. Homebrew /opt/homebrew/opt/openjdk@<ver> / /usr/local/opt/openjdk@<ver> → 5. ~/.sdkman/candidates/java/<ver>.* |
| Linux | 1. /usr/lib/jvm/java-<ver>-openjdk → 2. /usr/lib/jvm/java-<ver>-openjdk-amd64 → 3. /usr/lib/jvm/jdk-<ver> → 4. /usr/lib/jvm/jdk-<ver>.* (versioned) → 5. /usr/lib/jvm/java-<ver>.* (versioned) → 6. ~/.sdkman/candidates/java/<ver>.* |
| Windows | 1. %ProgramW6432% / %ProgramFiles% / C:\Program Files × Eclipse Adoptium / Java / Microsoft, matching jdk-<ver> then jdk-<ver>.* → 2. scoop openjdk<ver>/current |
The order is: precise version managers first, then standard install paths, then per-manager fallbacks. When multiple patch versions match (e.g. jdk-21.0.1, jdk-21.0.10), the highest one is selected via natural (numeric) ordering so 21.0.10 outranks 21.0.9.
:checkhealth jvm-env prints the detected manager presence and the path each configured version resolves to.
JDTLS_JAVA_HOME / GRADLE_JAVA_HOME are not standard variables recognized by jdtls or Gradle. They are this plugin's convention, and your jdtls spec must explicitly read them (see the wiring example above).
Why not reuse JAVA_HOME:
JAVA_HOME cannot separate jdtls from Gradle.JAVA_HOME affects unrelated tools.setup(opts)| Key | Type | Default | Description |
|---|---|---|---|
jdtls |
string | false | "21" |
JDK major version used to run jdtls. Pass false to skip. |
gradle |
string | false | "17" |
JDK major version used by Gradle. Pass false to skip. |
Versions are major-version strings (e.g. "21"). Exact versions (e.g. "21.0.1") also work if jenv / java_home can match them, but the major version is usually enough.
MIT