A Java language server that works from your build — no project import, no classpath setup.
Lathe is a Java language server — code intelligence, diagnostics, and run, test, and debug — driven by
your build rather than a separate project model. It is built on the JDK's own Java compiler, so its
analysis matches what javac sees.
If you've fought a Java LSP, the pain is usually project import and classpath or module-path config drifting from the build. Lathe skips that: its model comes straight from your Maven build — the exact configuration Maven compiles, tests, and runs with — so the setups hardest to get right in an editor (modular projects, annotation processors, plugins that add source roots) tend to just work.
Because the editor uses the build's own configuration, it stays in sync with it — diagnostics are what the compiler reports, and runs and tests replay the real launch without a Maven rebuild.
Setup is one extension registration, a first build, and a plugin line in your Neovim config.
Lathe ships a Neovim client, a VS Code extension, and an MCP server for AI coding agents.
Beyond Maven, Lathe also works from the OpenJDK make build.
A ~1-minute walkthrough — capture your Maven build's classpath once, then a zero-config Java IDE: completion and live diagnostics, running a modular main, debugging with live expression eval, and tests — all replayed from your build.
Lathe implements the standard LSP feature surface, plus run, test, and debug. Every capability is available to any LSP client; see Editors for the client that drives them and its key bindings.
| Feature | What it does | LSP method |
|---|---|---|
| Go to definition | jumps to local sources, unpacked dependency JAR sources, and JDK sources | textDocument/definition |
| Go to declaration | navigates to the overridden interface or abstract-method contract | textDocument/declaration |
| Implementation / subtypes | concrete implementations of a method, or all subtypes of a type across the workspace | textDocument/implementation |
| Find references | usages across the workspace | textDocument/references |
| Highlight uses | read/write uses of the symbol under the cursor, within the current file | textDocument/documentHighlight |
| Rename | renames a symbol and every reference across the whole reactor — all modules — as one atomic edit (locals, fields, methods incl. the override family, record components, constructors) | textDocument/rename · textDocument/prepareRename |
| Instantiation sites | where a type is instantiated (new AppServer(...)), from the type under the cursor |
workspace/executeCommand · lathe.instantiations |
| Hover | AST-resolved Javadoc, rendered as Markdown | textDocument/hover |
| Signature help | parameter lists for methods and constructors | textDocument/signatureHelp |
| Completion | types, methods, and variables, with automatic import insertion | textDocument/completion |
| Document / workspace symbols | file outline; workspace search with CamelCase-hump matching (ASF finds AbstractServerFactory) |
textDocument/documentSymbol, workspace/symbol |
| Type hierarchy | supertypes and subtypes of the symbol under the cursor, one level at a time | textDocument/prepareTypeHierarchy |
| Full type hierarchy | all transitive supertypes and subtypes of the type under the cursor at once, tagged by relation | workspace/executeCommand · lathe.typeHierarchy |
| Add missing imports | resolve every unimported type in the file in one pass — unambiguous names are added automatically, ambiguous ones prompt | workspace/executeCommand · lathe.missingImports |
| Call hierarchy | incoming and outgoing calls of a method | textDocument/prepareCallHierarchy |
| Semantic tokens | highlights static/deprecated members, enum constants, type parameters, annotations | textDocument/semanticTokens/full |
| Folding | classes, methods, blocks, and import groups | textDocument/foldingRange |
| Feature | What it does | LSP method |
|---|---|---|
| Diagnostics | javac errors and warnings exactly as configured in Maven, plus unused private members and locals |
textDocument/publishDiagnostics |
| Code actions | quick fixes and refactors: missing imports, add throws, wrap in try/catch, declare local, replace var with the inferred type, extract variable / constant / field, add a final field as a constructor parameter, stub a missing method |
textDocument/codeAction |
| Formatting (per-workspace) | whole-document google-java-format / AOSP, or the project's own Spotless formatter via mvn spotless:apply, or a custom external command — auto-detected per project |
textDocument/formatting |
Formatting follows a per-workspace style, auto-detected from the project's spotless-maven-plugin
by lathe:sync (a committed lathe-style.json or the editor's global default otherwise). It is
advertised only when a formatter resolves: the built-in google/aosp engine (fast, in-process); a
non-google Spotless formatter (eclipse, palantir, …) delegated to mvn spotless:apply on the edited
file (preferring mvnd → ./mvnw → mvn), so the editor applies the project's own formatter; or a custom
stdin/stdout command. Opt out with -Dlathe.spotless=false or a committed lathe-style.json. Live-edit
indentation follows the same style file and is otherwise a separate client concern.
| Feature | What it does | LSP method |
|---|---|---|
Run a main |
replays a main from captured .lathe/ bytecode — no Maven rebuild, live output |
workspace/executeCommand · lathe.run.main |
| Tests | discovers and runs tests (method, class, or package) from .lathe/ bytecode, with live output and a diagnostic on the failing assertion |
workspace/executeCommand · lathe.runnables.list, lathe.run.test |
| Debug | conditional breakpoints, stepping, variable inspection, and REPL expression evaluation over DAP | workspace/executeCommand · lathe.debug.*, then DAP |
| Run configuration | auto-applied overlays (JVM/program args, env, cwd, class-/module-path) plus named, selectable run configs (:LatheRun {name}, saved from the cursor) |
lathe.run.named · lathe.runconfigs.list · lathe.runconfig.save |
Run, test, and debug are Lathe extensions exposed through workspace/executeCommand (and the Debug
Adapter Protocol for debugging), not standard LSP methods. The Neovim commands (:LatheRun,
:LatheDebug, save/last/stop, the config picker) are in the
Neovim cheatsheet and run configuration.
| Feature | What it does | Command |
|---|---|---|
| New type | scaffolds a class / interface / record / enum / annotation / test, plus package-info and module-info — pick the kind, fuzzy-pick the destination package, and name it; the server resolves placement, writes the file, and opens it |
Neovim :LatheNew |
:LatheNew <kind> adds a type in the current package; bare :LatheNew runs a guided module/package
picker. The server owns placement — module, source root, package, and skeleton.
Walkthrough →.
Lathe keeps its model in step with your build. Open files are analysed live as you edit and save; when
sources or resources change outside the editor — a branch switch, a git pull, or an AI agent
editing files — Lathe reconciles in-process automatically, recompiling the changed files in dependency
order and cleaning up deletions with no Maven build. Only POM / module-structure changes prompt a
full refresh.
Lathe also works from the OpenJDK make build, not only Maven. The
lathe-openjdk-maven-plugin:sync goal reads the build's per-module javac invocations and the JDK you
built, and writes the same .lathe/ the language server consumes — so analysis is build-accurate across
all ~66 modules and the make/ build tools, in any LSP client. Code intelligence only for now; test
execution (jtreg) is not yet supported.
See the OpenJDK guide.
Lathe is a standard language server, so any LSP client can drive its build-derived intelligence. How much you get depends on the client:
| Editor | Integration | Reference |
|---|---|---|
| Neovim | Dedicated client (LSP + run/test/debug, scaffolding) | Neovim cheatsheet — install and keymaps |
| Emacs | Built-in Eglot (standard LSP, no plugin) | Emacs (Eglot) guide |
| VS Code | Extension — "Lathe for Java" (standard LSP) | VS Code guide |
The Neovim client adds Lathe-specific commands (:LatheRun,
:LatheNew, neotest, format-on-save) on top of LSP. Everything else is plain LSP, so a standard client
like Emacs + Eglot — or the VS Code extension — works with no extra configuration.
Lathe also drives AI coding agents. The same build-derived engine that powers the editor is exposed
over the Model Context Protocol by lathe-mcp-server, so an agent gets javac-accurate, cross-module
code intelligence instead of guessing from grep: compiler-truth diagnostics, navigation that follows
into dependencies and generated sources, safe reactor-wide rename, test replay without a Maven build,
and scoped recompile-and-verify of a change set. Works with any MCP client — Claude Code, OpenAI Codex
CLI, Gemini CLI.
Like the editor, it reads from a populated .lathe/, so run a build once first.
| Tool | What it does |
|---|---|
get_diagnostics |
compiler errors/warnings for one file — no Maven |
get_definition |
resolve a symbol to its definition, incl. dependencies/JDK/generated |
find_references |
every real use of a symbol across the reactor |
find_implementations |
implementers of an interface / overrides of a method |
call_hierarchy |
callers or callees of a method, across modules |
search_symbols |
find a type by name (CamelHumps) across reactor, dependencies, JDK |
describe_symbol |
signature, type, and javadoc for a symbol |
rename_symbol |
rename a symbol across the whole reactor, applied to disk |
run_test |
replay a test / class / package from captured bytecode — no build |
analyze_change |
pre-edit impact of a symbol: override family, production/test reference counts, affected modules, relevant tests |
verify_change |
recompile a change set in-process, report new diagnostics per module + the scoped mvn for cross-module impact — no build |
Full setup — registering with Claude Code, Codex, and Gemini, the result contract, and the freshness model — is in the AI agents guide.
- Java 21+ — the same JDK your Maven build uses.
- Maven 3.9+ — earlier 3.x releases are not supported. 3.9.x is verified.
Test run and debug have additional requirements (Surefire and JUnit Platform versions); see test-capture.md.
Set Lathe up once, in three steps.
1. Register the Lathe extension at your reactor root, as a Maven build extension — in
.mvn/extensions.xml, or in your root pom.xml under <build><extensions> (alongside any extensions
you already declare):
<extensions>
<extension>
<groupId>io.github.ag-libs</groupId>
<artifactId>lathe-maven-extension</artifactId>
<version>0.1.16</version>
</extension>
</extensions>See installation.md for details.
2. Generate the metadata once, and add .lathe/ to .gitignore:
mvn clean test -Dlathe.capture.only=trueThis captures every launch template — compiler params, the workspace manifest, and each module's
run/test launch — without running your test suite. -Dlathe.capture.only=true forks each module to
snapshot its launch template but skips executing the tests; clean forces a first compile through
Lathe.
Tip: the Maven Daemon (
mvnd) noticeably speeds up these builds — run it in place ofmvnwhere you can.
Note: if your build uses the Maven build cache extension, disable it for Lathe builds (
-Dmaven.build.cache.enabled=false) — a cache hit skips compilation, so Lathe would not see the real build and its captured configuration would go stale.
3. Add the Neovim client. It's the standalone ag-libs/lathe.nvim
plugin — install it like any other. With lazy.nvim:
{
"ag-libs/lathe.nvim",
ft = "java",
cmd = "LatheStart", -- also start it from a non-Java buffer
dependencies = { "mfussenegger/nvim-dap" }, -- optional: enables :LatheDebug
config = function()
require("lathe").setup() -- per-workspace style; `style`/`format_on_save` opts in the cheatsheet
-- Lathe adds no maps of its own; a starting set for commands with no Neovim default:
vim.keymap.set("n", "<leader>rr", "<cmd>LatheRun<cr>", { desc = "Run main under cursor" })
vim.keymap.set("n", "grN", "<cmd>LatheInstances<cr>", { desc = "Instantiation sites" })
vim.keymap.set("n", "grh", "<cmd>LatheTypeHierarchy<cr>", { desc = "Full type hierarchy" })
end,
}The config function calling setup() is required — without it the LSP server is never registered.
Standard LSP actions (go-to-definition, references, rename, …) use Neovim's built-in defaults, so they
work without extra maps. Full keymaps, formatting options, and the neotest test-runner integration are
in the Neovim cheatsheet. Requires Neovim 0.12+.
After that, it keeps up on its own: every Maven build (mvn test, verify, install) refreshes
Lathe's configuration, and Lathe reconciles source changes made outside the editor in-process.
The added build cost is marginal — Lathe runs your real javac and just records its parameters (plus a
one-time resolve of dependency and JDK sources). See
what the build writes for the details.
Lathe has a few moving parts, each documented in depth. In brief — full mechanics in How Lathe works:
- Build capture — every build records the exact compiler configuration and mirrors your compiled
classes into
.lathe/, which the language server reads. - Dependency & JDK sources — resolved and unpacked into
~/.cache/lathe/, so go-to-definition steps into library and JDK code. - Test capture — the test JVM is captured from inside your Surefire fork by live introspection and
replayed against
.lathe/without a Maven rebuild. Details → - Run & debug — runs and debug sessions replay the captured launch in a fresh JVM; customize it with overlays and named, selectable run configs. Details →
Lathe writes per-project metadata to .lathe/ (add it to .gitignore) and machine-wide,
regenerable data — the server, dependency/JDK sources, and indexes — to ~/.cache/lathe/ (relocate
with -Dlathe.cache=<dir>, safe to delete). What each holds:
what and where Lathe writes.
Lathe is active by default and skips automatically in CI. To turn it off for a whole team, commit the
property in the reactor pom.xml:
<properties>
<lathe.disabled>true</lathe.disabled>
</properties>Precedence:
| Condition | Effect |
|---|---|
<lathe.disabled>true</...> in the reactor POM |
disabled for everyone building the repo |
-Dlathe.disabled=true |
disabled regardless of other settings |
-Dlathe.disabled=false |
enabled, overrides CI |
CI environment variable is set |
Lathe does not run |
When the POM opts out, a developer can opt back in by creating an empty .lathe/ at the repo root: it
overrides the POM opt-out, so the next build wires Lathe up and repopulates the directory. (A
-Dlathe.disabled=true or CI kill stays absolute — .lathe/ does not override it.)
- Guides (editor-agnostic): how Lathe works · installation · run configuration · test capture
- Editor references: Neovim
- AI agents: MCP server guide
- Project: status · roadmap · design index · architecture
- No Lathe features, or a "launcher not found" notice — Lathe isn't active in the build yet. Run a
Maven build at the reactor root —
mvn process-test-classesis the quickest (it generates Lathe's metadata without running tests). If it still isn't working, confirm the Lathe extension is registered (see installation.md). - The server won't attach, or crashes — set
LATHE_DEBUG=1before launching your editor and check its LSP log (Neovim: cheatsheet). An unexpected exit is also surfaced as an editor notification pointing at the log. - Another Java language server (jdtls) misbehaves when Lathe is present — a co-running Eclipse JDT LS
scans Lathe's generated
.lathe/mirror and can treat it as duplicate projects. Add**/.lathe/**to itsjava.import.exclusions; see installation.md. - A processor or plugin needs extra JVM access in the editor — Lathe analyzes with an in-process
javac and drops a forked build's
-Joptions. Restore any it needs (or tune heap/GC) withLATHE_JVM_OPTS; see installation.md.
Feedback, bug reports, and questions are welcome — please open an issue.
If you would like to contribute code, please open an issue to discuss the change before opening a pull request. Thank you for trying Lathe.
Maintainers: see RELEASING.md for the release process.
Lathe is developed with the help of AI coding tools (primarily Claude Code).
AI-assisted contributions are reviewed by a human maintainer, who takes responsibility for every
change that lands, the same as for any hand-written code.
Commits produced with AI assistance carry a Co-Authored-By trailer so the provenance stays visible
in the git history.
AI-generated output is used only where it is compatible with this project's Apache-2.0 license, in line with the Apache Software Foundation's generative-tooling guidance.
Apache License 2.0 — see LICENSE.
