The short answer
sbt-eclipse generates Eclipse project files, JDTLS consumes them. That's the bridge. Everything else is configuration details.
If you're here, you've probably already tried Metals, hit a wall, and googled your way to disappointment. Here's the setup that actually works.
Context
Our project: Play Framework 3.x, Java (not Scala), sbt, 7 submodules, ~3,000 source files, heavy code generation (OpenAPI, Avro, WSDL). The rest of the team uses IntelliJ. I use Neovim.
There were exactly zero documented success stories for this combination online. The closest I found was a discussion on nvim-metals where people tried Metals and hit the same wall.
Why Metals won't work
Metals understands sbt natively. Great. But its Java support is minimal — no completions, no hover, no organize imports. A Metals maintainer explicitly stated: "I can't imagine the case when somebody wants to use Metals on a full-java project." Fair enough.
Why JDTLS doesn't work out of the box
JDTLS (Eclipse's Java language server) has full Java support, but only speaks Maven, Gradle, and Eclipse project formats. It has no idea what build.sbt is.
The solution
Make sbt produce what JDTLS expects: .classpath and .project files.
1. Install sbt-eclipse — without touching the repo
You're probably the only Neovim user on your team. No reason to commit IDE-specific build config. sbt supports user-level configuration:
~/.sbt/1.0/plugins/plugins.sbt
addSbtPlugin("com.github.sbt" % "sbt-eclipse" % "6.2.0")
~/.sbt/1.0/global.sbt
import com.typesafe.sbteclipse.core.EclipsePlugin.EclipseKeys
EclipseKeys.projectFlavor := EclipseProjectFlavor.Java
EclipseKeys.skipParents := false
EclipseKeys.withSource := true
EclipseKeys.preTasks := Seq(Compile / compile)
EclipseKeys.createSrc := EclipseCreateSrc.ValueSet(
EclipseCreateSrc.Unmanaged,
EclipseCreateSrc.Source,
EclipseCreateSrc.Resource,
EclipseCreateSrc.ManagedSrc,
EclipseCreateSrc.ManagedClasses,
EclipseCreateSrc.ManagedResources
)
Three things I learned the hard way:
| Gotcha | Why |
|---|---|
Don't use ThisBuild / scope |
sbt-eclipse reads keys at project level, not ThisBuild. The settings silently do nothing. |
ManagedSrc is critical |
Without it, generated sources (Play routes, OpenAPI codegen, Avro) don't appear in .classpath. ManagedClasses and ManagedResources alone are not enough. |
preTasks := Seq(Compile / compile) |
Code generation must run before Eclipse files are created, otherwise the managed source directories don't exist on disk yet. |
2. Generate Eclipse project files
sbt eclipse
This compiles first (triggering codegen), then generates .classpath and .project in root and every submodule. Add to .gitignore:
.classpath
.project
.settings/
3. Install and configure JDTLS
Install via Mason: :MasonInstall jdtls
Then configure root detection. This is the part that cost me the most time.
In a multi-module sbt project, every submodule has its own build.sbt and .project. If either of these is in your root_markers, JDTLS will root at the submodule instead of the repository root. It won't discover sibling modules. Cross-module imports silently fail.
Use .git as the primary anchor:
vim.lsp.config("jdtls", {
root_markers = {
{ "mvnw", "gradlew", "settings.gradle", "settings.gradle.kts", ".git" },
{ "pom.xml", "build.gradle", "build.gradle.kts", "build.xml" },
},
})
vim.lsp.enable("jdtls")
Nested arrays require Neovim 0.11.3+. First group that matches wins.
4. When things break: wipe the cache
JDTLS caches workspace data aggressively. If you change root detection config, or if projects aren't being discovered:
rm -rf ~/.cache/nvim/jdtls/workspace/
Restart Neovim. JDTLS re-imports everything from scratch.
What you get
Full Java IDE features on a Play Framework sbt project in Neovim:
- Code completion
- Go-to-definition (across modules)
- Find references
- Diagnostics
- Organize imports
- Rename refactoring
What you don't get
-
Running/debugging Play through JDTLS. Play's dev mode doesn't use a standard main method —
sbt runin a terminal, as usual. -
Automatic classpath updates. After dependency changes in
build.sbt, you need to re-runsbt eclipseand restart JDTLS. In practice this is a few times a month.
Normal coding, generated code changes, and branch switches (with same deps) need no action.
Why not just use IntelliJ?
IntelliJ handles Play + sbt out of the box. If that works for you, stay there. This guide exists because some of us prefer Neovim and, until now, there was no documented path to a working Java LSP for sbt projects.
Sources and tools:
- sbt-eclipse — the bridge that makes this possible
- nvim-lspconfig — JDTLS configuration for Neovim
- Mason.nvim — LSP server installer
- nvim-metals#503 — the discussion that confirms Metals won't work for this
- metals#1815 — Metals team on Java-only project support
Top comments (0)