Neovim + Java LSP on a Play Framework sbt Project — The Missing Guide
Source: Dev.to
The short answer
sbt-eclipse generates Eclipse project files, which JDTLS (Eclipse’s Java language server) consumes. 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.
Project context
- Play Framework 3.x, Java (not Scala)
- sbt with 7 sub‑modules, ~3 000 source files
- Heavy code generation (OpenAPI, Avro, WSDL)
- Team uses IntelliJ; you use Neovim
Metals understands sbt natively, but its Java support is minimal (no completions, hover, or organize‑imports). A Metals maintainer even said:
“I can’t imagine the case when somebody wants to use Metals on a full‑java project.”
JDTLS has full Java support, but only understands Maven, Gradle, and Eclipse project formats—it doesn’t know about build.sbt. The solution is to make sbt produce the files JDTLS expects: .classpath and .project.
Three things I learned the hard way
| Gotcha | Why |
|---|---|
Don’t use ThisBuild / scope | sbt‑eclipse reads keys at the project level, not ThisBuild. Settings placed under ThisBuild are silently ignored. |
ManagedSrc is critical | Without it, generated sources (Play routes, OpenAPI codegen, Avro) don’t appear in .classpath. ManagedClasses and ManagedResources alone aren’t 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. |
sbt‑eclipse configuration
Add the plugin globally
// ~/.sbt/1.0/plugins/plugins.sbt
addSbtPlugin("com.github.sbt" % "sbt-eclipse" % "6.2.0")Configure the plugin
// ~/.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
)Generate the Eclipse files
sbt eclipseThis command first compiles the project (triggering code generation), then creates .classpath and .project in the root and every submodule.
Add the generated files to .gitignore
.classpath
.project
.settings/Installing JDTLS via Mason
:MasonInstall jdtlsRoot detection configuration for Neovim
In a multi‑module sbt project each submodule has its own build.sbt and .project. If any of those appear in root_markers, JDTLS will treat the submodule as the workspace root, breaking cross‑module imports. 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")Note: Nested arrays require Neovim 0.11.3+. The first matching group wins.
If you change the root detection or modules aren’t being discovered, clear JDTLS’s cache:
rm -rf ~/.cache/nvim/jdtls/workspace/Then restart Neovim; JDTLS will re‑import everything from scratch.
Full Java IDE features in Neovim
- Code completion
- Go‑to‑definition (across modules)
- Find references
- Diagnostics
- Organize imports
- Rename refactoring
Running/debugging Play is still done via sbt (sbt run) in a terminal, because Play’s dev mode doesn’t use a standard main method.
Classpath updates – After changing dependencies in build.sbt, re‑run sbt eclipse and restart JDTLS. In practice this happens only a few times a month. Normal coding, generated‑code changes, and branch switches (with the same dependencies) require no extra action.
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 – discussion confirming Metals won’t work for this use‑case
- metals#1815 – Metals team’s stance on Java‑only project support