Language servers
katamari spawns servers lazily, the first time a file of that language is
opened, and looks for each one in this order: config override →
project-local convention (node_modules/.bin, .venv/bin) → PATH →
rustup which/mise which → katamari’s own managed install (below). If
none of those find anything, katamari auto-installs it — downloading
rust-analyzer’s prebuilt binary from GitHub releases, or running npm install/go install into a private prefix — with progress shown in the
status bar, no confirmation prompt, the same “it just works” experience
VSCode/Zed give you.
| Language | Server | Auto-install strategy |
|---|---|---|
| Rust | rust-analyzer |
prebuilt binary from GitHub releases |
| TypeScript/JavaScript | typescript-language-server |
npm install (bootstraps a private Node.js runtime first if no npm can be found anywhere) |
| Python | pyright-langserver |
npm install (same Node.js bootstrap fallback) |
| Go | gopls |
go install, requires an existing go toolchain — katamari won’t install Go itself |
| Kotlin | kotlin-lsp (JetBrains) |
prebuilt archive from JetBrains’ CDN, bundling its own JVM — no external Java needed |
| Java | jdtls (Eclipse JDT LS) |
prebuilt tarball from download.eclipse.org — needs an external JDK 21+ (katamari won’t install a JVM) |
kotlin-lsp is JetBrains’ own server (the fwcd/kotlin-language-server
community project is unmaintained as of this writing) and is still alpha
quality: on a project with no Gradle wrapper yet cached, its first hover,
go-to-definition, or diagnostics pull can take tens of seconds while it
resolves the classpath and indexes the project in the background —
katamari’s ktmr lsp-check retries handle this the same way they do
rust-analyzer’s own cold-start indexing. kotlin-lsp only implements
LSP 3.17’s pull model (textDocument/diagnostic), never the unsolicited
publishDiagnostics push notifications every other server here sends;
katamari’s gutter/]d/[d/--diagnostics flow now pulls on Kotlin’s
behalf after every open/change and re-publishes the answer through the same
path a push would use, so error/warning highlighting works for Kotlin files
too — the only user-visible difference from a push server is that the very
first diagnostics for a freshly-opened file can lag behind hover/go-to
readiness while indexing finishes, since an early pull during that window
can legitimately come back empty. One more side effect worth knowing about:
importing a Gradle project spawns a Gradle daemon, which — by Gradle’s own
design — keeps running after kotlin-lsp itself exits (including after a
ktmr doctor probe). That’s normal daemon reuse, not a leak of katamari’s;
gradle --stop (or killing the GradleDaemon process) reclaims it.
jdtls needs a JDK 21+ on the machine — JAVA_HOME is honored first, then
PATH, then mise which java — katamari installs the server itself but
never a JVM to run it on; ktmr lsp doctor prints a jdk: note under the
Java row naming the JDK it found and its version, saying so if the one it
found is too old, or reporting not found if there’s no JDK anywhere.
First-open indexing can take a while on a large Maven/Gradle repo, during
which hover/go-to-definition may time out until the import finishes. Its
per-workspace index lives under
$XDG_STATE_HOME/katamari/jdtls-workspaces/ (~/.local/state/katamari/…
if unset) — separate from the managed-server install below, and likewise
safe to delete to force a reindex.
Managed installs live under ~/.local/share/katamari/servers/
($XDG_DATA_HOME/katamari/servers/ if set), one version-stamped
subdirectory per server — never touched by anything but katamari, and safe
to delete entirely (everything reinstalls on demand).
[lsp.servers.<id>] in config (see Configuration)
overrides any of these with an explicit command, taking priority over
every lookup including auto-install.
To disable auto-install and just get the old “here’s the install command”
status-bar hint instead:
[lsp]auto_install = falseCustom language servers
Section titled “Custom language servers”[lsp.servers.<id>] isn’t limited to overriding one of the six built-in
languages above — any <id> of your own choosing defines a server for a
filetype katamari has no built-in support for, as long as it claims at
least one file extension:
[lsp.servers.ruby]command = "solargraph"args = ["stdio"]extensions = ["rb"]root_markers = ["Gemfile"]extensions is what makes ruby claim .rb files at all — a custom id
with no extensions is just an inert config entry (indistinguishable from
a plain built-in override with none of the new fields set). A leading .
is accepted and stripped (extensions = [".rb"] and ["rb"] are
equivalent), and each entry is trimmed of surrounding whitespace. A custom
claim on an extension one of the six built-in languages already owns is
dropped, the built-in always winning; if two custom ids claim the same
extension, whichever id sorts first alphabetically wins instead; and
extensions set on an id that’s itself one of the six built-in language
names (e.g. [lsp.servers.rust]) is ignored outright, since <id> is what
decides override-vs-custom — all three cases warn once to stderr, and
ktmr lsp doctor’s custom-server table flags an affected entry with a note
saying why its extensions don’t route anywhere. root_markers finds this
id’s workspace root the same way a built-in language’s nearest-marker tier
does — the closest ancestor
directory containing one of the listed files, falling back to the
repository root if it’s empty or nothing matches; there’s no built-in-style
“workspace of workspaces” tier for a custom id; that logic is
adapter-specific knowledge (a Cargo [workspace] table, a go.work, a
Gradle settings.gradle) this module has no way to generalize to a server
it’s never heard of. language_id overrides the LSP languageId
announced in didOpen, for the rare server that expects something other
than the <id> key itself.
A custom server is never auto-installed — command must already be
reachable on its own, and ktmr lsp doctor reports it (in a second table
beneath the built-in one) as found or not found the same way it does for a
built-in language; ktmr lsp install doesn’t support a custom id.
initialization_options (TOML, converted to JSON at resolve time) works on
a built-in override too, not just a custom server — e.g. sending
rust-analyzer settings through [lsp.servers.rust].
ktmr lsp manages installs directly, without waiting for a server to be
needed:
ktmr lsp doctor # where each language's server resolves from today (no installs triggered) — see the Health check chapter for the fuller reportktmr lsp install <language> # force an install into katamari's managed prefix (rust/typescript/python/go/kotlin/java/all)ktmr lsp update # reinstall any pinned server that's fallen behind the current pin