Skip to content

LSP Tools ​

Lemon has three language-tooling paths:

  • lsp: the model-facing code-intelligence tool (diagnostics, navigation, symbols, rename, code actions, raw requests) backed by supervised language servers.
  • lsp_diagnostics: the model-facing coding-agent tool for one-file diagnostics through local checkers and post-edit baseline/delta checks.
  • supervised LSP sessions: BEAM-owned stdio language-server processes managed through the in-process LemonLsp.ServerManager API, which lsp uses.

Status and support bundles expose capabilities, counters, hashes, and install hints, not raw file contents, raw workspace roots, raw session ids, diagnostic output, or server I/O.

Model-Facing lsp Tool ​

lsp talks to a real language server. The server is chosen by file extension (see the install guide); its workspace root is the nearest ancestor of the file, inside the working directory, that holds one of the server's root markers (for clangd compile_commands.json or compile_flags.txt), else the working directory.

ParameterMeaning
actionRequired. diagnostics, definition, type_definition, implementation, references, hover, symbols, rename, rename_file, reload, capabilities, code_actions, status or request.
fileFile path relative to the working directory, or absolute.
line1-based line for position actions.
symbolText on line to position at (first occurrence; name#N for the Nth). Bare identifiers match whole words; case-insensitive matching is the fallback.
column1-based column (code points) when symbol is not given. Without either, the first non-blank character is used.
queryWorkspace symbol search text, the code action to apply (index or title substring), or the method for request.
new_nameNew symbol name for rename, or destination path for rename_file.
applytrue writes rename/rename_file/code_actions edits. Default false (preview/list).
timeoutSeconds, default 20, clamped to 5..300.
payloadJSON params for request; otherwise params are built from file/line/symbol.

Examples:

json
{"action":"references","file":"src/util.c","line":3,"symbol":"add_numbers"}
{"action":"rename","file":"src/main.c","line":6,"symbol":"add_numbers","new_name":"sum_numbers","apply":true}
{"action":"code_actions","file":"src/main.c","line":8,"apply":true,"query":"0"}
{"action":"symbols","query":"add_num"}
{"action":"request","file":"src/main.c","line":6,"symbol":"total","query":"textDocument/documentHighlight"}

Behavior:

  • Locations print as path:line:col relative to the working directory with a one-line excerpt around each. Positions sent to the server are converted to UTF-16 offsets; printed columns are code points.
  • references prints context for the first 50 results, plain locations up to 500, and the total count. Workspace symbols are capped at 200 and diagnostics at 100 per file. Output above ~48 KB is saved to the session spill directory and referenced from the result.
  • diagnostics uses pull diagnostics when the server advertises them, else waits (within timeout) for the server to publish diagnostics for the document version it was just sent.
  • diagnostics with file: "*" snapshots published diagnostics across running clients in this session/workspace, without starting servers or waiting for fresh analysis. It examines at most 32 clients and 200 files per client and returns at most 100 diagnostics total.
  • rename_file takes file and new_name (a destination path). It previews by default; apply: true applies workspace/willRenameFiles edits before moving the file through the same locked/checkpointed WorkspaceEdit path, then sends workspace/didRenameFiles when supported. Existing destinations are not overwritten.
  • reload restarts the server for file, or all running servers in this session when file is omitted or "*". capabilities with file reports the selected server's advertised capabilities.
  • symbols with file lists document symbols; with only query it searches the workspace through the session's running servers (or servers whose root marker is in the working directory).
  • code_actions uses the symbol's range, the column, or the whole line, and sends the diagnostics overlapping it. Applying resolves the action through codeAction/resolve when needed and runs its command through workspace/executeCommand, applying edits the server sends back.
  • Applied edits support text edits and create/rename/delete file operations. Every affected path is locked, validated like write/edit targets and checkpointed before the first write; text edits are validated (overlaps, ranges, snippets) before any file is written; files are replaced atomically and the session's read tracker is refreshed, so a later edit is not rejected as stale. Applying calls are serialized with other mutating tools in the session; read-only calls run in parallel. A code action retains chronological checkpoint_ids for its direct edit and every server-requested batch, including partial failure or cancellation. Rewinding restores these batches in reverse order, recovering the pre-action state for repeated as well as disjoint paths. A failed action reports success: false; its receipt still identifies any committed paths and recoverable checkpoints. If an interrupted server-requested edit has no final outcome, affected_paths identifies captured paths that may have changed, with an uncertainty warning; the receipt does not claim those paths stayed untouched. changed_files still reports confirmed mutations from completed batches. Failed filesystem steps also report created parent directories, leftover temporary files, files created before a failed write, and partially deleted trees. Created parents and partially deleted trees are identified by their root path, including when the failing operation was the first in the batch.
  • status lists every registered server as installed, missing (with its install hint) or running for this session.

One initialized OS process is shared per (server id, workspace root, effective server configuration), across durable coding sessions and direct manager users. Session-scoped clients hold reference-counted leases. Native turns hand off clients only after saving and every resource handoff succeed; otherwise resources are disposed. Resumed turns bind the same durable identity. Workspace-level raw requests and symbol searches also rebind clients to the current owner before use, so owner death releases the rebound client's lease. Reset rebuilds tools for the fresh session id and releases old-id leases. Explicit abort signals the run before disposal; explicit stop, logical delete, unexpected owner death and 10 idle minutes also release that client's lease, without stopping a server another session still owns.

The last release starts an idle grace period: configure [runtime.tools.lsp] idle_timeout_ms = 30000 (the default); 0 stops immediately. Reacquisition cancels the timer. ServerManager.release_session/2 releases one lease, whereas stop_session/1 and the tool's reload force a shared restart.

The manager reads the latest on-disk text during synchronization, owns each URI's monotonically increasing version, opens it once and closes it only after its last document owner releases it. Published diagnostics are available to every owner and delivered as {LemonLsp.ServerManager, :diagnostics, session_id, uri, entry} messages. A crash fails every in-flight request with a clear error, not an automatic request replay; the next use starts and initializes a fresh process. Configuration changes use a different pool key on the next catalog refresh/tool call. A missing server binary is reported with its install hint. lsp remains dangerous because some servers build or run project code.

Server Configuration ​

The catalog is layered as built-in defaults → ~/.lemon/config.toml → project .lemon/config.toml, using the normal Lemon config loader. Configure servers under [runtime.tools.lsp.servers.<id>]; known ids override individual fields and unknown ids add servers (requiring command and non-empty extensions, or its alias file_types). Server ids should use lowercase letters and underscores.

toml
[runtime.tools.lsp.servers.clangd]
command = "/opt/llvm/bin/clangd"
args = ["--background-index"]
root_markers = ["compile_commands.json", ".git"]
initialization_options = { clangdFileStatus = true }
env = { CLANGD_FLAGS = "" }

[runtime.tools.lsp.servers.pyright]
disabled = true

[runtime.tools.lsp.servers.my_language]
command = "my-language-server"
args = ["--stdio"]
extensions = [".mylang"]
language_id = "my-language"

Lists replace inherited lists; nested tables merge with project values winning. language_id sets the LSP language id for all configured extensions; without it, built-in per-extension ids remain intact. Extensions are case-insensitive and may omit the leading dot. Disabled servers are absent from selection and lookup. The documented LEMON_LSP_*_COMMAND variables still take priority over TOML commands. Explicit reload also restarts an already-running shared server.

CodingAgent.Tools.Lsp.Client coordinates synchronization and initialization; only LemonLsp.ServerManager opens LSP processes. The separately named lsp_diagnostics and LspFormatter helpers execute local checker/formatter commands, not LSP servers, and do not use this catalog.

lsp_diagnostics Tool ​

lsp_diagnostics accepts a file path relative to the active working directory:

json
{"path":"lib/example.ex"}

write, edit, and patch can opt into post-edit diagnostics with:

json
{"diagnostics":true}

The post-edit path compares a pre-edit baseline with the post-edit result and reports newly introduced diagnostics separately from pre-existing issues. Missing tools or unsupported file types return structured skipped results instead of failing the edit.

External checkers use the deadline-aware process executor. A timeout terminates the checker process group and descendants, rather than only the BEAM caller.

Local Checker Install Guide ​

The preview runner uses local workspace tools when they are present:

LanguageFile extensionsChecker pathInstall notes
Elixir.ex, .exs, .heexsyntax parser, then mix compile --return-errors when a mix.exs workspace existsInstall Elixir and Mix.
JavaScript.js, .cjs, .mjsnode --checkInstall Node.js.
TypeScript.ts, .tsx, .jsxtsc --noEmit --pretty false when tsconfig.json exists, preferring workspace node_modules/.bin/tsc when presentInstall TypeScript in the workspace or on PATH.
Python.pypython3 -m py_compile or python -m py_compileInstall Python 3.
Rust.rscargo check --message-format=short when Cargo.toml existsInstall Rust and Cargo.
Go.gogo vet ./... when go.mod exists (does not execute tests)Install Go.
C/C++.c, .h, .cc, .cpp, .cxx, .hh, .hpp, .hxxclang/gcc/cc or clang++/g++/c++ with -fsyntax-onlyInstall a C/C++ compiler; install clangd separately for supervised LSP sessions.

The runner intentionally skips TypeScript, Rust, and Go when the expected workspace marker is missing. C/C++ syntax checks run per file when a compiler is available. That keeps single-file edits predictable outside a real project.

Language Server Install Guide ​

The supervised stdio manager registers these language servers:

Server idLanguageCommandOverride env varInstall notes
elixir_lsElixirelixir-lsLEMON_LSP_ELIXIR_LS_COMMANDInstall ElixirLS and put elixir-ls on PATH; alternatives elixir-ls-language-server, language_server.sh, and launcher paths such as launch.sh are also supported. Lemon sets ELS_MODE=language_server for this server.
typescript_language_serverTypeScript/JavaScripttypescript-language-server --stdioLEMON_LSP_TYPESCRIPT_COMMANDnpm install -g typescript typescript-language-server.
pyrightPythonpyright-langserver --stdioLEMON_LSP_PYRIGHT_COMMANDnpm install -g pyright.
rust_analyzerRustrust-analyzerLEMON_LSP_RUST_ANALYZER_COMMANDInstall rust-analyzer and put it on PATH.
goplsGogoplsLEMON_LSP_GOPLS_COMMANDgo install golang.org/x/tools/gopls@latest.
clangdC/C++clangdLEMON_LSP_CLANGD_COMMANDInstall clangd and provide compile_commands.json or compile_flags.txt in the workspace for useful diagnostics.
tlaplusTLA+tlapm_lsp --stdioLEMON_LSP_TLAPLUS_COMMANDInstall TLAPM and put tlapm_lsp on PATH.
zlsZLSzlsLEMON_LSP_ZLS_COMMANDInstall ZLS and put zls on PATH.
vscode_html_language_serverHTMLvscode-html-language-server --stdioLEMON_LSP_VSCODE_HTML_LANGUAGE_SERVER_COMMANDInstall with npm install -g vscode-langservers-extracted.
vscode_css_language_serverCSSvscode-css-language-server --stdioLEMON_LSP_VSCODE_CSS_LANGUAGE_SERVER_COMMANDInstall with npm install -g vscode-langservers-extracted.
vscode_json_language_serverJSONvscode-json-language-server --stdioLEMON_LSP_VSCODE_JSON_LANGUAGE_SERVER_COMMANDInstall with npm install -g vscode-langservers-extracted.
svelteSveltesvelteserver --stdioLEMON_LSP_SVELTE_COMMANDInstall with npm install -g svelte-language-server.
vue_language_serverVuevue-language-server --stdioLEMON_LSP_VUE_LANGUAGE_SERVER_COMMANDInstall with npm install -g @vue/language-server.
astroAstroastro-ls --stdioLEMON_LSP_ASTRO_COMMANDInstall with npm install -g @astrojs/language-server.
jdtlsEclipse JDT LSjdtlsLEMON_LSP_JDTLS_COMMANDInstall Eclipse JDT LS and put jdtls on PATH.
kotlin_lspKotlin LSPkotlin-lsp --stdioLEMON_LSP_KOTLIN_LSP_COMMANDInstall JetBrains kotlin-lsp and put kotlin-lsp on PATH.
metalsMetalsmetalsLEMON_LSP_METALS_COMMANDInstall Metals with Coursier: cs install metals.
hlsHaskell Language Serverhaskell-language-server-wrapper --lspLEMON_LSP_HLS_COMMANDInstall Haskell Language Server with ghcup install hls.
ocamllspOCaml LSPocamllspLEMON_LSP_OCAMLLSP_COMMANDInstall with opam install ocaml-lsp-server.
erlanglsErlang LSerlang_lsLEMON_LSP_ERLANGLS_COMMANDInstall Erlang LS and put erlang_ls on PATH.
gleamGleamgleam lspLEMON_LSP_GLEAM_COMMANDInstall Gleam and put gleam on PATH.
ruby_lspRuby LSPruby-lspLEMON_LSP_RUBY_LSP_COMMANDInstall with gem install ruby-lsp.
bashlsBash Language Serverbash-language-server startLEMON_LSP_BASHLS_COMMANDInstall with npm install -g bash-language-server.
lua_language_serverLua Language Serverlua-language-serverLEMON_LSP_LUA_LANGUAGE_SERVER_COMMANDInstall lua-language-server and put it on PATH.
intelephenseIntelephenseintelephense --stdioLEMON_LSP_INTELEPHENSE_COMMANDInstall with npm install -g intelephense.
yamllsYAML Language Serveryaml-language-server --stdioLEMON_LSP_YAMLLS_COMMANDInstall with npm install -g yaml-language-server.
terraformlsTerraform LSterraform-ls serveLEMON_LSP_TERRAFORMLS_COMMANDInstall HashiCorp terraform-ls and put it on PATH.
dockerlsDockerfile Language Serverdocker-langserver --stdioLEMON_LSP_DOCKERLS_COMMANDInstall with npm install -g dockerfile-language-server-nodejs.
nixdnixdnixdLEMON_LSP_NIXD_COMMANDInstall with nix profile install nixpkgs#nixd.
olsOdin Language ServerolsLEMON_LSP_OLS_COMMANDInstall OLS and put ols on PATH.
dartlsDart Analysis Serverdart language-server --protocol=lspLEMON_LSP_DARTLS_COMMANDInstall the Dart SDK and put dart on PATH.
marksmanMarksmanmarksman serverLEMON_LSP_MARKSMAN_COMMANDInstall Marksman and put marksman on PATH.
texlabTexlabtexlabLEMON_LSP_TEXLAB_COMMANDInstall Texlab and put texlab on PATH.
graphqlGraphQL Language Servergraphql-lsp server -m streamLEMON_LSP_GRAPHQL_COMMANDInstall with npm install -g graphql-language-service-cli.
prismalsPrisma Language Serverprisma-language-server --stdioLEMON_LSP_PRISMALS_COMMANDInstall with npm install -g @prisma/language-server.
vimlsVim Language Servervim-language-server --stdioLEMON_LSP_VIMLS_COMMANDInstall with npm install -g vim-language-server.
sourcekit_lspSourceKit-LSPsourcekit-lspLEMON_LSP_SOURCEKIT_LSP_COMMANDInstall the Swift toolchain and put sourcekit-lsp on PATH.
csharp_lsC# Language Servercsharp-lsLEMON_LSP_CSHARP_LS_COMMANDInstall with dotnet tool install --global csharp-ls.
taploTaplotaplo lsp stdioLEMON_LSP_TAPLO_COMMANDInstall with cargo install taplo-cli --locked --features lsp.
elmElm Language Serverelm-language-server --stdioLEMON_LSP_ELM_COMMANDInstall with npm install -g @elm-tooling/elm-language-server.
clojure_lspClojure LSPclojure-lspLEMON_LSP_CLOJURE_LSP_COMMANDInstall clojure-lsp and put it on PATH.
soliditySolidity Language Servernomicfoundation-solidity-language-server --stdioLEMON_LSP_SOLIDITY_COMMANDInstall with npm install -g @nomicfoundation/solidity-language-server.

The registry assigns each extension to one primary server and omits supplemental linters. Language ids are available through LemonLsp.Servers.language_id/1; unknown extensions (including extensionless Dockerfile) return "plaintext". Dockerfile LSP registration covers .dockerfile files.

Override env vars can point at either a command on PATH or an absolute local executable path. Public status redacts absolute paths to command basenames.

Supervised Session API ​

The in-process session API is defined by LemonLsp.ServerManager: start/initialize a session, open/change/close documents, send requests or notifications, and stop the session. It is not exposed as control-plane RPCs.

Session status reports request/response counts, notification counts, open and known document counts, redacted document URI hashes, diagnostic batch counts, and severity counts. It does not expose raw text, raw URIs, workspace paths, server logs, or diagnostic messages.

Language-server stderr is discarded by the launcher so log writes can never interleave with JSON-RPC frames or leak private workspace paths to the caller console; the parser still discards non-LSP prefixes and only records redacted frame counters from valid JSON-RPC messages. When a language-server launcher spawns child processes, stop_session also terminates those children so wrapper-style servers do not outlive the BEAM-owned session. A session started with an owner pid stops when that process exits. If a JSON-RPC request times out, the manager by default treats the session as unhealthy, replies with :request_timeout, terminates the launcher plus descendants, and records the recent session as :request_timeout instead of leaving a stuck port alive; requests made with on_timeout: :cancel send $/cancelRequest and keep the session.

The manager answers server-initiated requests as a headless client (configuration defaults, workspace folders, progress and capability registration). workspace/applyEdit is accepted only during request_with_edits/4, which requires an apply_edit callback returning {:ok, receipt} or {:error, reason}. The callback runs outside the manager through the caller's mutation/checkpoint path; the server receives applied: true only after the filesystem application succeeds. Partial failures and expired requests receive applied: false. Receipts/outcomes are returned in application order. Only one edit-capturing operation may run per server session; ordinary requests remain available during callbacks. Timeout or caller death cancels the operation and stops its in-flight callback.

The manager records both push diagnostics from textDocument/publishDiagnostics notifications and pull diagnostics from textDocument/diagnostic responses. Public status keeps only URI hashes, diagnostic counts, severity counts, and timestamps; the latest raw diagnostics per document stay in-process for document_diagnostics/2.

Focused Test Coverage ​

From the umbrella root:

bash
mix test apps/coding_agent/test/coding_agent/tools/lsp_diagnostics_test.exs \
  apps/coding_agent/test/coding_agent/tools/lsp_test.exs \
  apps/coding_agent/test/coding_agent/tools/lsp_clangd_test.exs \
  apps/coding_agent/test/coding_agent/tools/lsp \
  apps/lemon_lsp/test --seed 1

These exercise supervised server startup, framing, server-initiated requests, document diagnostics, timeout cleanup, wrapper-child termination, the lsp tool against a scripted fake server, and (when clangd is on PATH) against real clangd. They are not proof that a particular language server is installed or correctly configured on an operator's machine.

Released under the MIT License.