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

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
end

Router 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
end

PubSub 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)

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]
end

Stream 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