Current section
Files
Jump to
Current section
Files
lib/credo_language_server.ex
defmodule CredoLanguageServer do
@moduledoc """
LSP implementation for Credo.
"""
use GenLSP
alias GenLSP.ErrorResponse
alias GenLSP.Enumerations.{
CodeActionKind,
DiagnosticSeverity,
ErrorCodes,
TextDocumentSyncKind
}
alias GenLSP.Notifications.{
Exit,
Initialized,
TextDocumentDidChange,
TextDocumentDidOpen,
TextDocumentDidSave
}
alias GenLSP.Requests.{Initialize, Shutdown, TextDocumentCodeAction}
alias GenLSP.Structures.{
CodeActionContext,
CodeActionOptions,
CodeActionParams,
Diagnostic,
DidOpenTextDocumentParams,
InitializeParams,
InitializeResult,
Position,
Range,
SaveOptions,
ServerCapabilities,
TextDocumentIdentifier,
TextDocumentItem,
TextDocumentSyncOptions,
WorkDoneProgressBegin,
WorkDoneProgressEnd
}
alias CredoLanguageServer.Cache, as: Diagnostics
def start_link(args) do
{args, opts} =
Keyword.split(args, [:cache, :task_supervisor, :runtime_supervisor])
GenLSP.start_link(__MODULE__, args, opts)
end
@impl true
def init(lsp, args) do
cache = Keyword.fetch!(args, :cache)
task_supervisor = Keyword.fetch!(args, :task_supervisor)
runtime_supervisor = Keyword.fetch!(args, :runtime_supervisor)
{:ok,
assign(lsp,
exit_code: 1,
cache: cache,
documents: %{},
refresh_refs: %{},
task_supervisor: task_supervisor,
runtime_supervisor: runtime_supervisor
)}
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()
},
code_action_provider: %CodeActionOptions{
code_action_kinds: [CodeActionKind.quick_fix()]
}
},
server_info: %{name: "Credo"}
}, assign(lsp, root_uri: root_uri)}
end
def handle_request(
%TextDocumentCodeAction{
params: %CodeActionParams{
context: %CodeActionContext{diagnostics: diagnostics},
text_document: %TextDocumentIdentifier{uri: uri}
}
},
lsp
) do
code_actions =
for %Diagnostic{data: %{"check" => check}} = diagnostic <- diagnostics,
check =
CredoLanguageServer.Check.new(
check: check,
diagnostic: diagnostic,
uri: uri,
document: lsp.assigns.documents[uri]
),
action <- CredoLanguageServer.CodeActionable.fetch(check) do
action
end
{:reply, code_actions, lsp}
end
def handle_request(%Shutdown{}, lsp) do
{:reply, nil, assign(lsp, exit_code: 0)}
end
def handle_request(%{method: method}, lsp) do
GenLSP.warning(lsp, "[Credo] Method Not Found: #{method}")
{:reply,
%ErrorResponse{
code: ErrorCodes.method_not_found(),
message: "Method Not Found: #{method}"
}, lsp}
end
@impl true
def handle_notification(%Initialized{}, lsp) do
GenLSP.log(lsp, "[Credo] LSP Initialized!")
token =
8
|> :crypto.strong_rand_bytes()
|> Base.url_encode64(padding: false)
|> binary_part(0, 8)
GenLSP.notify(lsp, %GenLSP.Notifications.DollarProgress{
params: %GenLSP.Structures.ProgressParams{
token: token,
value: %WorkDoneProgressBegin{
kind: "begin",
title: "Initializing Credo..."
}
}
})
working_dir = URI.parse(lsp.assigns.root_uri).path
GenLSP.log(lsp, "[Credo] Booting runime...")
{:ok, runtime} =
DynamicSupervisor.start_child(
lsp.assigns.runtime_supervisor,
{CredoLanguageServer.Runtime,
working_dir: working_dir, parent: self()}
)
lsp = assign(lsp, runtime: runtime)
task =
Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn ->
with false <-
wait_until(fn ->
CredoLanguageServer.Runtime.ready?(runtime)
end) do
GenLSP.error(lsp, "Failed to start runtime")
raise "Failed to boot runtime"
end
GenLSP.log(lsp, "[Credo] Runtime ready...")
with false <-
wait_until(fn ->
match?(
{:ok, _},
CredoLanguageServer.Runtime.call(
runtime,
{Application, :ensure_all_started, [:credo]}
)
)
end) do
GenLSP.error(lsp, "Failed to start the Credo application")
raise "Failed to start credo"
end
GenLSP.log(lsp, "[Credo] Credo started in runtime...")
CredoLanguageServer.Runtime.call(
runtime,
{GenServer, :call,
[Credo.CLI.Output.Shell, {:suppress_output, true}]}
)
count = refresh(lsp)
publish(lsp)
count
end)
{:noreply, put_in(lsp.assigns.refresh_refs[task.ref], token)}
end
def handle_notification(
%TextDocumentDidSave{
params: %GenLSP.Structures.DidSaveTextDocumentParams{
text: text,
text_document: %{uri: uri}
}
},
lsp
) do
token =
8
|> :crypto.strong_rand_bytes()
|> Base.url_encode64(padding: false)
|> binary_part(0, 8)
GenLSP.notify(lsp, %GenLSP.Notifications.DollarProgress{
params: %GenLSP.Structures.ProgressParams{
token: token,
value: %WorkDoneProgressBegin{
kind: "begin",
title: "Credo",
message: "Finding issues..."
}
}
})
task =
Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn ->
Diagnostics.clear(lsp.assigns.cache)
count = refresh(lsp)
publish(lsp)
count
end)
{:noreply,
lsp
|> (&put_in(&1.assigns.documents[uri], String.split(text, "\n"))).()
|> (&put_in(&1.assigns.refresh_refs[task.ref], token)).()}
end
def handle_notification(%TextDocumentDidChange{}, lsp) do
for task <- Task.Supervisor.children(lsp.assigns.task_supervisor) do
Process.exit(task, :kill)
end
Task.Supervisor.start_child(lsp.assigns.task_supervisor, fn ->
Diagnostics.clear(lsp.assigns.cache)
publish(lsp)
end)
{:noreply, lsp}
end
def handle_notification(
%TextDocumentDidOpen{
params: %DidOpenTextDocumentParams{
text_document: %TextDocumentItem{text: text, uri: uri}
}
},
lsp
) do
{:noreply, put_in(lsp.assigns.documents[uri], String.split(text, "\n"))}
end
def handle_notification(%Exit{}, lsp) do
System.halt(lsp.assigns.exit_code)
{:noreply, lsp}
end
def handle_notification(_notification, lsp) do
{:noreply, lsp}
end
def handle_info({ref, count}, %{assigns: %{refresh_refs: refs}} = lsp)
when is_map_key(refs, ref) do
Process.demonitor(ref, [:flush])
{token, refs} = Map.pop(refs, ref)
GenLSP.notify(lsp, %GenLSP.Notifications.DollarProgress{
params: %GenLSP.Structures.ProgressParams{
token: token,
value: %WorkDoneProgressEnd{
kind: "end",
message: "Found #{count} issues"
}
}
})
{:noreply, assign(lsp, refresh_refs: refs)}
end
def handle_info(
{:DOWN, ref, :process, _pid, _reason},
%{assigns: %{refresh_refs: refs}} = lsp
)
when is_map_key(refs, ref) do
{token, refs} = Map.pop(refs, ref)
GenLSP.notify(lsp, %GenLSP.Notifications.DollarProgress{
params: %GenLSP.Structures.ProgressParams{
token: token,
value: %WorkDoneProgressEnd{
kind: "end"
}
}
})
{:noreply, assign(lsp, refresh_refs: refs)}
end
def handle_info({:log, message}, lsp) do
GenLSP.log(lsp, String.trim(message))
{:noreply, lsp}
end
def handle_info(_, lsp) do
{:noreply, lsp}
end
defp refresh(lsp) do
dir = URI.parse(lsp.assigns.root_uri).path
issues =
CredoLanguageServer.Runtime.call(
lsp.assigns.runtime,
{:_credo_language_server_private_credo, :issues, [dir]}
)
for issue <- issues do
diagnostic = %Diagnostic{
range: %Range{
start: %Position{
line: issue.line_no - 1,
character: (issue.column || 1) - 1
},
end: %Position{
line: issue.line_no - 1,
character: issue.column || 1
}
},
severity: category_to_severity(issue.category),
data: %{check: issue.check, file: issue.filename},
message: """
#{issue.message}
## Explanation
#{CredoLanguageServer.Runtime.call(lsp.assigns.runtime, {issue.check, :explanations, []})[:check]}
"""
}
Diagnostics.put(
lsp.assigns.cache,
Path.join(lsp.assigns.root_uri, issue.filename)
|> URI.parse()
|> Map.get(:path),
diagnostic
)
end
Enum.count(issues)
end
defp publish(lsp) do
for {file, diagnostics} <- Diagnostics.get(lsp.assigns.cache) do
GenLSP.notify(lsp, %GenLSP.Notifications.TextDocumentPublishDiagnostics{
params: %GenLSP.Structures.PublishDiagnosticsParams{
uri: "file://#{file}",
diagnostics: diagnostics
}
})
end
end
defp category_to_severity(:refactor), do: DiagnosticSeverity.error()
defp category_to_severity(:warning), do: DiagnosticSeverity.warning()
defp category_to_severity(:design), do: DiagnosticSeverity.information()
defp category_to_severity(:consistency),
do: DiagnosticSeverity.information()
defp category_to_severity(:readability),
do: DiagnosticSeverity.information()
defp wait_until(cb) do
wait_until(120, cb)
end
defp wait_until(0, _cb) do
false
end
defp wait_until(n, cb) do
if cb.() do
true
else
Process.sleep(1000)
wait_until(n - 1, cb)
end
end
end