ElixirLsp
ElixirLsp is an Elixir-native, protocol-focused Language Server Protocol (LSP) toolkit.
It is use-case agnostic and focuses on robust protocol handling, transport, and ergonomic server building blocks.
Features
-
Typed JSON-RPC/LSP message structs (
Request,Notification,Response,ErrorResponse) -
LSP framing and streaming decode (
Content-Lengthaware) -
Production-safe stdio transport (
:content_lengthmode by default) -
Handler DSL (
use ElixirLsp.Server) withdefrequest/2anddefnotification/2 -
Router DSL (
use ElixirLsp.Router) with request/notification/catch-all handlers -
Request lifecycle support: cancellation (
$/cancelRequest), timeouts, and telemetry spans -
Cooperative cancellation helpers:
with_cancel/2andcheck_cancel!/1 -
Handler context helpers:
reply/2,error/4,notify/3,canceled?/1,with_cancel/2,check_cancel!/1 -
State toolkit for open docs/workspace + text sync (
didOpen/didChange/didClose) - Strict/lenient lifecycle modes for out-of-order client events
- Capability builder DSL
-
LSP helper types with coercion (
from_map/to_map) -
Typed type generation macro (
ElixirLsp.Types.deflsp_type/2) -
Elixir-native response builders (
ElixirLsp.Responses) -
Automatic outbound key normalization (
snake_caseto wirecamelCase) - Middleware pipeline + built-ins
Enumerable/Collectablestream pipeline helpers-
First-class
Phoenix.PubSubfanout for diagnostics/progress/events - In-memory test harness
-
Option validation with
NimbleOptions Inspect/String.Charsimplementations for core structsmix lsp.gen.handlerscaffold task-
OTP
child_spec/1helpers
Install
{:elixir_lsp, "~> 0.2.1"}Quick start
request = ElixirLsp.request(1, :initialize, %{"processId" => nil})
wire = request |> ElixirLsp.encode() |> IO.iodata_to_binary()
{:ok, [message], _state} = ElixirLsp.recv(wire)Handler DSL
defmodule MyHandler do
use ElixirLsp.Server
defrequest :initialize do
with_cancel(ctx, fn ->
reply(ctx, ElixirLsp.Responses.initialize(%{hover_provider: true}, name: "my-lsp", version: "0.2.1"))
end)
end
defnotification {:text_document_did_open, %{"textDocument" => doc}} do
check_cancel!(ctx)
{:ok, Map.put(state, :last_opened_uri, doc["uri"])}
end
endRouter DSL
Route blocks expose params, ctx, state.
Aliases _params, _ctx, _state are also available when values are intentionally unused.
defmodule MyHandler do
use ElixirLsp.Router
capabilities do
hover true
completion resolve_provider: false
end
on_request :initialize do
reply(ctx, %{
"capabilities" => __MODULE__.server_capabilities()
})
end
on_request :text_document_hover do
{:reply, %{"contents" => "Hello"}, _state}
end
on_notification :text_document_did_open do
{:ok, state}
end
endPubSub fanout
{:ok, _} = ElixirLsp.PubSub.start_link(name: ElixirLsp.PubSub)
:ok = ElixirLsp.PubSub.subscribe(ElixirLsp.PubSub, "elixir_lsp:diagnostics")
{:ok, _server} =
ElixirLsp.Server.start_link(
handler: MyHandler,
pubsub: [name: ElixirLsp.PubSub, topic_prefix: "elixir_lsp"],
send: fn framed -> IO.binwrite(:stdio, framed) end
)Transport
Recommended production path:
ElixirLsp.run_stdio(handler: MyHandler, init: %{})Explicit modes:
ElixirLsp.Transport.Stdio.run(handler: MyHandler, init: %{}, mode: :content_length)
ElixirLsp.Transport.Stdio.run(handler: MyHandler, init: %{}, mode: :chunk)State lifecycle mode
lenient = ElixirLsp.State.new(mode: :lenient) # default
strict = ElixirLsp.State.new(mode: :strict):lenient: ignores out-of-order events (for exampledidChangewithoutdidOpen):strict: raises on lifecycle mismatches
Typed map interop
{:ok, action} = ElixirLsp.Types.from_map(ElixirLsp.Types.CodeAction, incoming_map)
outgoing_map = ElixirLsp.Types.to_map(action)Type generation:
defmodule MyTypes do
require ElixirLsp.Types
ElixirLsp.Types.deflsp_type PublishDiagnostics, required: [:uri], optional: [:version, :diagnostics]
endStream pipeline
stream =
ElixirLsp.decode_chunks(chunks)
messages = Enum.to_list(ElixirLsp.pipeline_messages(stream))Supervision
children = [
ElixirLsp.child_spec(
name: MyServer,
handler: MyHandler,
handler_arg: %{},
send: fn framed -> IO.binwrite(:stdio, framed) end
)
]Test harness
{:ok, harness} = ElixirLsp.TestHarness.start_link(handler: MyHandler)
:ok = ElixirLsp.TestHarness.request(harness, 1, :shutdown, %{})
outbound_messages = ElixirLsp.TestHarness.drain_outbound(harness)Scaffolding
mix lsp.gen.handler Hover