DEV Community

Nicolas Medda
Nicolas Medda

Posted on

Neovim + Java LSP on a Play Framework sbt Project — The Missing Guide

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")
Enter fullscreen mode Exit fullscreen mode

~/.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
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This compiles first (triggering codegen), then generates .classpath and .project in root and every submodule. Add to .gitignore:

.classpath
.project
.settings/
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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 run in a terminal, as usual.
  • Automatic classpath updates. After dependency changes in build.sbt, you need to re-run sbt eclipse and 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:

Top comments (0)