View Source GenLSP behaviour (gen_lsp v0.11.1)
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
endLink 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
@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
@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
@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.")
@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
}
})
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.
@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
})
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.
Starts a GenLSP process that is linked to the current process.
options
Options
:buffer- Thepid/0or name of theGenLSP.Bufferprocess.:assigns- Thepid/0or name of theGenLSP.Assignsprocess.:task_supervisor- The Task.Supervisor for the task queue:name(atom/0) - Used for name registration as described in the "Name registration" section in the documentation forGenServer.
@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.")