# ElixirLsp
`ElixirLsp` is a protocol-focused, use-case agnostic LSP library for Elixir.
It provides:
- Native typed LSP/JSON-RPC messages
- Framing + stream decode for stdio/socket chunks
- `ElixirLsp.Transport.Stdio` production-safe stdio loop (content-length aware by default)
- Router DSL (`use ElixirLsp.Router`) with request/notification handlers
- Request lifecycle features: cancellation (`$/cancelRequest`) and timeouts
- Handler context helpers (`reply`, `error`, `notify`, `canceled?`)
- State toolkit (`ElixirLsp.State`) for text sync/document tracking
- Capability DSL (`capabilities do ... end`)
- LSP helper structs (`Range`, `Diagnostic`, `TextEdit`, `WorkspaceEdit`, `CodeAction`) with `from_map`/`to_map`
- Middleware pipeline and built-in middlewares
- In-memory test harness (`ElixirLsp.TestHarness`)
- OTP embedding via `ElixirLsp.child_spec/1`
## Install
```elixir
{:elixir_lsp, "~> 0.1.0"}
```
## Native API
```elixir
request = ElixirLsp.request(1, :initialize, %{"processId" => nil})
wire = request |> ElixirLsp.encode() |> IO.iodata_to_binary()
{:ok, [message], _state} = ElixirLsp.recv(wire)
```
## Router DSL
`params`, `ctx`, `state` are available in route blocks. Aliases `_params`, `_ctx`, `_state` are also available for ergonomic unused bindings.
```elixir
defmodule MyHandler do
use ElixirLsp.Router
capabilities do
hover true
completion resolve_provider: false
end
on_request :initialize do
ElixirLsp.HandlerContext.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
```
## Production stdio transport
Recommended default:
```elixir
ElixirLsp.run_stdio(handler: MyHandler, init: %{})
```
Equivalent explicit call:
```elixir
ElixirLsp.Transport.Stdio.run(handler: MyHandler, init: %{}, mode: :content_length)
```
Alternative mode (`:chunk`) is available for raw fixed-size forwarding.
## State lifecycle mode
```elixir
state = ElixirLsp.State.new(mode: :lenient) # default
# or
strict_state = ElixirLsp.State.new(mode: :strict)
```
In `:lenient`, out-of-order lifecycle events like `didChange` without `didOpen` are ignored. In `:strict`, they raise.
## Typed map interop
```elixir
{:ok, action} = ElixirLsp.Types.from_map(ElixirLsp.Types.CodeAction, incoming_map)
outgoing_map = ElixirLsp.Types.to_map(action)
```
## 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)
```