A Java language server that works from your Maven build — no project import, no classpath setup.
Lathe is a Java language server for Maven projects — code intelligence, diagnostics, and run, test, and
debug. It is built on the JDK's own Java compiler, so its analysis matches what javac sees.
If you've fought a Java LSP in your editor, the pain is usually project import and classpath or module-path config drifting from the build. Lathe's project model comes straight from your actual Maven build: it captures the exact configuration Maven compiles, tests, and runs with, and works from it directly — so the setups that are hardest to get right in an editor tend to just work. This shows most on modular Java projects: reactor type discovery, exported-package visibility, and module-aware completion follow your module graph as the build defines it. Annotation processors and plugins that add source roots or change how a module compiles are handled the same way.
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 client is planned.
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 locals, parameters, type parameters, fields, methods (incl. the override family), record components, and constructors, and their references — as one atomic edit | 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 | import missing type · add all missing imports for the file · add throws clause · wrap with try/catch · declare local variable · replace var with the inferred type · extract variable / constant / field (incl. replace all occurrences) · stub a missing method |
textDocument/codeAction |
| Formatting (opt-in) | whole-document google-java-format with import cleanup — off by default | textDocument/formatting |
Full-document formatting is opt-in: the server advertises textDocument/formatting only when a
client enables the google formatter, so Lathe never rewrites a project whose style contract isn't
Google Java Format. Live-editing indentation is a separate, always-on client concern — see the editor
guide to configure both.
| 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 | overlay JVM args, program args, environment, working directory, and class-/module-path per module | — (lathe-run.json overlays) |
Run, test, and debug are Lathe extensions exposed through workspace/executeCommand (and the Debug
Adapter Protocol for debugging), not standard LSP methods.
| 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 file's package; run :LatheNew with no arguments for a
guided flow that fuzzy-picks the destination module and package. The server owns every Java/Maven
decision — module, source root, package, and skeleton. Full walkthrough in the
Neovim cheatsheet.
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 detects it and offers to refresh. Changed resources are copied in without a
build.
| Editor | Status | Reference |
|---|---|---|
| Neovim | Supported | Neovim cheatsheet — install and keymaps |
| VS Code | Planned | — |
- Java 21+ — the same JDK your Maven build uses.
- Maven 3.x
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):
<extension>
<groupId>io.github.ag-libs</groupId>
<artifactId>lathe-maven-extension</artifactId>
<version>0.1.6</version>
</extension>See installation.md for details.
2. Generate the metadata once, and add .lathe/ to .gitignore:
mvn clean test -Dlathe.capture.>This captures every launch template — compiler params, the workspace manifest, and each module's
run/test launch — without running your test suite. -Dlathe.capture.> 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. The build unpacks it into ~/.cache/lathe/current/neovim; point your
plugin manager at that directory. With lazy.nvim:
{
dir = vim.fn.expand("~/.cache/lathe/current/neovim"), -- installed by the Lathe build
ft = "java",
cmd = "LatheStart", -- also start it from a non-Java buffer
dependencies = { "mfussenegger/nvim-dap" }, -- optional: enables :LatheDebug
config = function()
require("lathe").setup() -- formatter/indent are opt-in; see 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 the editor watches for changes made outside it. 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 an overlay. 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:
| Condition | Effect |
|---|---|
CI environment variable is set |
Lathe does not run |
-Dlathe.skip=true |
disabled regardless of other settings |
-Dlathe.skip=false |
enabled, overrides CI |
- Guides (editor-agnostic): how Lathe works · installation · run configuration · test capture
- Editor references: Neovim
- 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.
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.
Apache License 2.0 — see LICENSE.