https://github.com/clang-engineer/jvm-env.nvim
Auto-detect installed JDKs into Neovim env vars (separate jdtls / Gradle JDKs)
https://github.com/clang-engineer/jvm-env.nvim
java jdtls lua neovim neovim-plugin
Last synced: about 22 hours ago
JSON representation
Auto-detect installed JDKs into Neovim env vars (separate jdtls / Gradle JDKs)
- Host: GitHub
- URL: https://github.com/clang-engineer/jvm-env.nvim
- Owner: clang-engineer
- License: mit
- Created: 2026-06-17T14:08:55.000Z (10 days ago)
- Default Branch: main
- Last Pushed: 2026-06-18T00:08:16.000Z (9 days ago)
- Last Synced: 2026-06-18T02:10:09.810Z (9 days ago)
- Topics: java, jdtls, lua, neovim, neovim-plugin
- Language: Lua
- Size: 37.1 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
- awesome-neovim - clang-engineer/jvm-env.nvim - Auto-detects installed JDKs by major version, with separate environment variables for jdtls and Gradle. (Programming Languages Support / Diagnostics)
README
# jvm-env.nvim
[](https://github.com/clang-engineer/jvm-env.nvim/actions/workflows/lint.yml)
[](https://github.com/clang-engineer/jvm-env.nvim/actions/workflows/test.yml)
[](LICENSE)
> **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).
```lua
require("jvm-env").setup({ jdtls = "21", gradle = "17" })
-- → vim.env.JDTLS_JAVA_HOME =
-- → vim.env.GRADLE_JAVA_HOME =
```
## What it does
- Tries `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.
- Writes the resolved path into `vim.env.JDTLS_JAVA_HOME` and `vim.env.GRADLE_JAVA_HOME`.
- If nothing is found, warns via `vim.notify` and keeps going — never blocks other startup.
- Run `:checkhealth jvm-env` to see which managers are visible and which paths each version resolves to.
## What it does NOT do
- **Does not start any LSP.** Your jdtls spec (nvim-jdtls, LazyVim Java extras, etc.) is still yours to write.
- **Does not install JDKs.** It only finds JDKs you already installed (via jEnv, SDKMAN, brew, apt, scoop, …).
- **Does not touch standard `JAVA_HOME`.** Your shell's `JAVA_HOME` is left alone; jvm-env uses two dedicated variables instead, deliberately avoiding conflicts with other tools.
## Why
A common Java setup:
- jdtls wants a recent JDK (e.g. 21) to run reliably.
- The project itself targets an LTS JDK (e.g. 17) for Spring Boot 3.x, Android, etc.
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**.
## Adjacent tools
| 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](https://github.com/mfussenegger/nvim-jdtls/discussions/548) and [mason.nvim discussion #1893](https://github.com/mason-org/mason.nvim/discussions/1893), where the answer is consistently "set `JAVA_HOME` yourself." jvm-env fills that gap.
## Requirements
- Neovim 0.10+ (uses `vim.uv` and the new `vim.health` API).
- One JDK manager / install path supported below (jEnv, SDKMAN, Homebrew, apt, scoop, Adoptium, …).
## Install
[lazy.nvim](https://github.com/folke/lazy.nvim):
```lua
{
"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"`).
## Usage
### Default
```lua
require("jvm-env").setup()
-- fills both env vars with the defaults (jdtls=21, gradle=17)
```
### Custom versions
```lua
require("jvm-env").setup({ jdtls = "21", gradle = "17" })
```
### Disabling one side
Pass `false` to skip detection for that env var entirely (no warning, no value set):
```lua
require("jvm-env").setup({ jdtls = "21", gradle = false })
-- only JDTLS_JAVA_HOME is touched
```
### Wiring up jdtls (LazyVim + nvim-jdtls)
`lua/plugins/java.lua`:
```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`.
### Per-project versions via `.nvim.lua`
`.nvim.lua` (loaded per directory via Neovim 0.9+ `exrc`):
```lua
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.
## Detection order
| OS | Order |
|---|---|
| **macOS** | 1. `jenv prefix ` (if `jenv` is installed) → 2. `jenv versions --bare` filtered to `.*` (exact fallback) → 3. `/usr/libexec/java_home -v ` → 4. Homebrew `/opt/homebrew/opt/openjdk@` / `/usr/local/opt/openjdk@` → 5. `~/.sdkman/candidates/java/.*` |
| **Linux** | 1. `/usr/lib/jvm/java--openjdk` → 2. `/usr/lib/jvm/java--openjdk-amd64` → 3. `/usr/lib/jvm/jdk-` → 4. `/usr/lib/jvm/jdk-.*` (versioned) → 5. `/usr/lib/jvm/java-.*` (versioned) → 6. `~/.sdkman/candidates/java/.*` |
| **Windows** | 1. `%ProgramW6432%` / `%ProgramFiles%` / `C:\Program Files` × Eclipse Adoptium / Java / Microsoft, matching `jdk-` then `jdk-.*` → 2. scoop `openjdk/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.
## Environment variable naming
`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`:
- A single `JAVA_HOME` cannot separate jdtls from Gradle.
- Overriding the shell `JAVA_HOME` affects unrelated tools.
## API
### `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.
## License
MIT