View Source GenLSP behaviour (gen_lsp v0.10.0)

GenLSP is an OTP behaviour for building processes that implement the Language Server Protocol.

examples

Examples

Credo language server.
defmodule Credo.Lsp do
  @moduledoc """
  LSP implementation for Credo.
  """
  use GenLSP

  alias GenLSP.Enumerations.TextDocumentSyncKind

  alias GenLSP.Notifications.{
    Exit,
    Initialized,
    TextDocumentDidChange,
    TextDocumentDidClose,
    TextDocumentDidOpen,
    TextDocumentDidSave
  }

  alias GenLSP.Requests.{Initialize, Shutdown}

  alias GenLSP.Structures.{
    InitializeParams,
    InitializeResult,
    SaveOptions,
    ServerCapabilities,
    TextDocumentSyncOptions
  }

  alias Credo.Lsp.Cache, as: Diagnostics

  def start_link(args) do
    GenLSP.start_link(__MODULE__, args, [])
  end

  @impl true
  def init(lsp, args) do
    cache = Keyword.fetch!(args, :cache)

    {:ok, assign(lsp, exit_code: 1, cache: cache)}
  end

  @impl true
  def handle_request(%Initialize{params: %InitializeParams{root_uri: root_uri}}, lsp) do
    {:reply,
     %InitializeResult{
       capabilities: %ServerCapabilities{
         text_document_sync: %TextDocumentSyncOptions{
           open_close: true,
           save: %SaveOptions{include_text: true},
           change: TextDocumentSyncKind.full()
         }
       },
       server_info: %{name: "Credo"}
     }, assign(lsp, root_uri: root_uri)}
  end

  def handle_request(%Shutdown{}, lsp) do
    {:noreply, assign(lsp, exit_code: 0)}
  end

  @impl true
  def handle_notification(%Initialized{}, lsp) do
    GenLSP.log(lsp, :log, "[Credo] LSP Initialized!")
    Diagnostics.refresh(lsp.assigns.cache, lsp)
    Diagnostics.publish(lsp.assigns.cache, lsp)

    {:noreply, lsp}
  end

  def handle_notification(%TextDocumentDidSave{}, lsp) do
    Task.start_link(fn ->
      Diagnostics.clear(lsp.assigns.cache)
      Diagnostics.refresh(lsp.assigns.cache, lsp)
      Diagnostics.publish(lsp.assigns.cache, lsp)
    end)

    {:noreply, lsp}
  end

  def handle_notification(%TextDocumentDidChange{}, lsp) do
    Task.start_link(fn ->
      Diagnostics.clear(lsp.assigns.cache)
      Diagnostics.publish(lsp.assigns.cache, lsp)
    end)

    {:noreply, lsp}
  end

  def handle_notification(%note{}, lsp)
      when note in [TextDocumentDidOpen, TextDocumentDidClose] do
    {:noreply, lsp}
  end

  def handle_notification(%Exit{}, lsp) do
    System.halt(lsp.assigns.exit_code)

    {:noreply, lsp}
  end

  def handle_notification(_thing, lsp) do
    {:noreply, lsp}
  end
end


defmodule Credo.Lsp.Cache do
  @moduledoc """
  Cache for Credo diagnostics.
  """
  use Agent

  alias GenLSP.Structures.{
    Diagnostic,
    Position,
    PublishDiagnosticsParams,
    Range
  }

  alias GenLSP.Notifications.TextDocumentPublishDiagnostics

  def start_link(_) do
    Agent.start_link(fn -> Map.new() end)
  end

  def refresh(cache, lsp) do
    dir = URI.new!(lsp.assigns.root_uri).path

    issues = Credo.Execution.get_issues(Credo.run(["--strict", "--all", "#{dir}/**/*.ex"]))

    GenLSP.log(lsp, :info, "[Credo] Found #{Enum.count(issues)} issues")

    for issue <- issues do
      diagnostic = %Diagnostic{
        range: %Range{
          start: %Position{line: issue.line_no - 1, character: issue.column || 0},
          end: %Position{line: issue.line_no, character: 0}
        },
        severity: category_to_severity(issue.category),
        message: """
        #{issue.message}

        ## Explanation

        #{issue.check.explanations()[:check]}
        """
      }

      put(cache, Path.absname(issue.filename), diagnostic)
    end
  end

  def get(cache) do
    Agent.get(cache, & &1)
  end

  def put(cache, filename, diagnostic) do
    Agent.update(cache, fn cache ->
      Map.update(cache, Path.absname(filename), [diagnostic], fn v ->
        [diagnostic | v]
      end)
    end)
  end

  def clear(cache) do
    Agent.update(cache, fn cache ->
      for {k, _} <- cache, into: Map.new() do
        {k, []}
      end
    end)
  end

  def publish(cache, lsp) do
    for {file, diagnostics} <- get(cache) do
      GenLSP.notify(lsp, %TextDocumentPublishDiagnostics{
        params: %PublishDiagnosticsParams{
          uri: "file://#{file}",
          diagnostics: diagnostics
        }
      })
    end
  end

  def category_to_severity(:refactor), do: 1
  def category_to_severity(:warning), do: 2
  def category_to_severity(:design), do: 3
  def category_to_severity(:consistency), do: 4
  def category_to_severity(:readability), do: 4
end

Link to this section Summary

Callbacks

The callback responsible for handling normal messages.

The callback responsible for handling notifications from the client.

The callback responsible for handling requests from the client.

The callback responsible for initializing the process.

Functions

Send a window/logMessage error notification to the client.

Send a window/logMessage info notification to the client.

Send a window/logMessage log notification to the client.

Sends a notification to the client from the LSP process.

Sends a notification from the client to the LSP process.

Sends a request to the client from the LSP process.

Sends a request from the client to the LSP process.

Starts a GenLSP process that is linked to the current process.

Send a window/logMessage error notification to the client.

Link to this section Callbacks

Link to this callback

handle_info(message, state)

View Source
@callback handle_info(message :: any(), state) :: {:noreply, state}
when state: GenLSP.LSP.t()

The callback responsible for handling normal messages.

Receives the message as the first argument and the LSP token GenLSP.LSP.t/0 as the second.

usage

Usage

@impl true
def handle_info(message, lsp) do
  # handle the message

  {:noreply, lsp}
end
Link to this callback

handle_notification(notification, state)

View Source
@callback handle_notification(notification :: term(), state) :: {:noreply, state}
when state: GenLSP.LSP.t()

The callback responsible for handling notifications from the client.

Receives the notification struct as the first argument and the LSP token GenLSP.LSP.t/0 as the second.

usage

Usage

@impl true
def handle_notification(%Initialized{}, lsp) do
  # handle the notification

  {:noreply, lsp}
end
Link to this callback

handle_request(request, state)

View Source
@callback handle_request(request :: term(), state) ::
  {:reply, reply :: term(), state} | {:noreply, state}
when state: GenLSP.LSP.t()

The callback responsible for handling requests from the client.

Receives the request struct as the first argument and the LSP token GenLSP.LSP.t/0 as the second.

usage

Usage

@impl true
def handle_request(%Initialize{params: %InitializeParams{root_uri: root_uri}}, lsp) do
  {:reply,
   %InitializeResult{
     capabilities: %ServerCapabilities{
       text_document_sync: %TextDocumentSyncOptions{
         open_close: true,
         save: %SaveOptions{include_text: true},
         change: TextDocumentSyncKind.full()
       }
     },
     server_info: %{name: "MyLSP"}
   }, assign(lsp, root_uri: root_uri)}
end
@callback init(lsp :: GenLSP.LSP.t(), init_arg :: term()) :: {:ok, GenLSP.LSP.t()}

The callback responsible for initializing the process.

Receives the GenLSP.LSP.t/0 token as the first argument and the arguments that were passed to GenLSP.start_link/3 as the second.

usage

Usage

@impl true
def init(lsp, args) do
  some_arg = Keyword.fetch!(args, :some_arg)

  {:ok, assign(lsp, static_assign: :some_assign, some_arg: some_arg)}
end

Link to this section Functions

@spec error(GenLSP.LSP.t(), String.t()) :: :ok

Send a window/logMessage error notification to the client.

See GenLSP.Enumerations.MessageType.error/0.

usage

Usage

GenLSP.error(lsp, "Failed to compiled!")
@spec info(GenLSP.LSP.t(), String.t()) :: :ok

Send a window/logMessage info notification to the client.

See GenLSP.Enumerations.MessageType.info/0.

usage

Usage

GenLSP.info(lsp, "Compilation complete!")
@spec log(GenLSP.LSP.t(), String.t()) :: :ok

Send a window/logMessage log notification to the client.

See GenLSP.Enumerations.MessageType.log/0.

usage

Usage

GenLSP.log(lsp, "Starting compilation.")
Link to this function

notify(map, notification)

View Source
@spec notify(GenLSP.LSP.t(), notification :: any()) :: :ok

Sends a notification to the client from the LSP process.

usage

Usage

GenLSP.notify(lsp, %TextDocumentPublishDiagnostics{
  params: %PublishDiagnosticsParams{
    uri: "file://#{file}",
    diagnostics: diagnostics
  }
})
Link to this function

notify_server(pid, notification)

View Source
@spec notify_server(pid(), message) :: message when message: term()

Sends a notification from the client to the LSP process.

Generally used by the GenLSP.Communication.Adapter implementation to forward messages from the buffer to the LSP process.

You shouldn't need to use this to implement a language server.

Link to this function

request(map, request, timeout \\ :infinity)

View Source
@spec request(GenLSP.LSP.t(), request :: any(), timeout :: atom() | non_neg_integer()) ::
  any()

Sends a request to the client from the LSP process.

usage

Usage

GenLSP.request(lsp, %ClientRegisterCapability{
  id: System.unique_integer([:positive]),
  params: params
})
Link to this function

request_server(pid, request)

View Source
@spec request_server(pid(), message) :: message when message: term()

Sends a request from the client to the LSP process.

Generally used by the GenLSP.Communication.Adapter implementation to forward messages from the buffer to the LSP process.

You shouldn't need to use this to implement a language server.

Link to this function

start_link(module, init_args, opts)

View Source

Starts a GenLSP process that is linked to the current process.

options

Options

  • :buffer - The pid/0 or name of the GenLSP.Buffer process.

  • :name (atom/0) - Used for name registration as described in the "Name registration" section in the documentation for GenServer.

@spec warning(GenLSP.LSP.t(), String.t()) :: :ok

Send a window/logMessage error notification to the client.

See GenLSP.Enumerations.MessageType.warning/0.

usage

Usage

GenLSP.warning(lsp, "Variable `foo` is unused.")