# 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-Length` aware)
- Production-safe stdio transport (`:content_length` mode by default)
- Handler DSL (`use ElixirLsp.Server`) with `defrequest/2` and `defnotification/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/2` and `check_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_case` to wire `camelCase`)
- Middleware pipeline + built-ins
- `Enumerable`/`Collectable` stream pipeline helpers
- First-class `Phoenix.PubSub` fanout for diagnostics/progress/events
- In-memory test harness
- Option validation with `NimbleOptions`
- `Inspect`/`String.Chars` implementations for core structs
- `mix lsp.gen.handler` scaffold task
- OTP `child_spec/1` helpers
## Install
```elixir
{:elixir_lsp, "~> 0.2.1"}
```
## Quick start
```elixir
request = ElixirLsp.request(1, :initialize, %{"processId" => nil})
wire = request |> ElixirLsp.encode() |> IO.iodata_to_binary()
{:ok, [message], _state} = ElixirLsp.recv(wire)
```
## Handler DSL
```elixir
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
end
```
## Router DSL
Route blocks expose `params`, `ctx`, `state`.
Aliases `_params`, `_ctx`, `_state` are also available when values are intentionally unused.
```elixir
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
end
```
## PubSub fanout
```elixir
{: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:
```elixir
ElixirLsp.run_stdio(handler: MyHandler, init: %{})
```
Explicit modes:
```elixir
ElixirLsp.Transport.Stdio.run(handler: MyHandler, init: %{}, mode: :content_length)
ElixirLsp.Transport.Stdio.run(handler: MyHandler, init: %{}, mode: :chunk)
```
## State lifecycle mode
```elixir
lenient = ElixirLsp.State.new(mode: :lenient) # default
strict = ElixirLsp.State.new(mode: :strict)
```
- `:lenient`: ignores out-of-order events (for example `didChange` without `didOpen`)
- `:strict`: raises on lifecycle mismatches
## Typed map interop
```elixir
{:ok, action} = ElixirLsp.Types.from_map(ElixirLsp.Types.CodeAction, incoming_map)
outgoing_map = ElixirLsp.Types.to_map(action)
```
Type generation:
```elixir
defmodule MyTypes do
require ElixirLsp.Types
ElixirLsp.Types.deflsp_type PublishDiagnostics, required: [:uri], optional: [:version, :diagnostics]
end
```
## Stream pipeline
```elixir
stream =
ElixirLsp.decode_chunks(chunks)
messages = Enum.to_list(ElixirLsp.pipeline_messages(stream))
```
## Supervision
```elixir
children = [
ElixirLsp.child_spec(
name: MyServer,
handler: MyHandler,
handler_arg: %{},
send: fn framed -> IO.binwrite(:stdio, framed) end
)
]
```
## Test harness
```elixir
{:ok, harness} = ElixirLsp.TestHarness.start_link(handler: MyHandler)
:ok = ElixirLsp.TestHarness.request(harness, 1, :shutdown, %{})
outbound_messages = ElixirLsp.TestHarness.drain_outbound(harness)
```
## Scaffolding
```bash
mix lsp.gen.handler Hover
```