README.md

# 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)
```