Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: signature help #396

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions lib/next_ls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ defmodule NextLS do
alias GenLSP.Requests.TextDocumentFormatting
alias GenLSP.Requests.TextDocumentHover
alias GenLSP.Requests.TextDocumentReferences
alias GenLSP.Requests.TextDocumentSignatureHelp
alias GenLSP.Requests.WorkspaceApplyEdit
alias GenLSP.Requests.WorkspaceSymbol
alias GenLSP.Structures.ApplyWorkspaceEditParams
Expand All @@ -41,6 +42,8 @@ defmodule NextLS do
alias GenLSP.Structures.Range
alias GenLSP.Structures.SaveOptions
alias GenLSP.Structures.ServerCapabilities
alias GenLSP.Structures.SignatureHelp
alias GenLSP.Structures.SignatureHelpParams
alias GenLSP.Structures.SymbolInformation
alias GenLSP.Structures.TextDocumentIdentifier
alias GenLSP.Structures.TextDocumentItem
Expand All @@ -53,6 +56,7 @@ defmodule NextLS do
alias NextLS.DiagnosticCache
alias NextLS.Progress
alias NextLS.Runtime
alias NextLS.SignatureHelp

require NextLS.Runtime

Expand Down Expand Up @@ -165,6 +169,9 @@ defmodule NextLS do
"alias-refactor"
]
},
signature_help_provider: %GenLSP.Structures.SignatureHelpOptions{
trigger_characters: ["(", ","]
},
hover_provider: true,
workspace_symbol_provider: true,
document_symbol_provider: true,
Expand Down Expand Up @@ -810,6 +817,36 @@ defmodule NextLS do
{:reply, nil, lsp}
end

def handle_request(
%TextDocumentSignatureHelp{params: %SignatureHelpParams{text_document: %{uri: uri}, position: position}},
lsp
) do
text = Enum.join(lsp.assigns.documents[uri], "\n")

signature_help =
case SignatureHelp.fetch(text, {position.line + 1, position.character + 1}) do
{:ok, {mod, name, param_index}} ->
docs =
dispatch(lsp.assigns.registry, :runtimes, fn entries ->
[result] =
for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do
Runtime.call(runtime, {Code, :fetch_docs, [mod]})
end

result
end)

docs
|> SignatureHelp.format(name, param_index)
|> List.first()

{:error, :not_found} ->
nil
end

{:reply, signature_help, lsp}
end

def handle_request(%Shutdown{}, lsp) do
{:reply, nil, assign(lsp, exit_code: 0)}
end
Expand Down
48 changes: 48 additions & 0 deletions lib/next_ls/helpers/ast_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,52 @@ defmodule NextLS.ASTHelpers do

top(zipper, acc, callback)
end

defmodule Function do
@moduledoc false

def find_remote_function_call_within(ast, {line, column}) do
position = [line: line, column: column]

result =
ast
|> Zipper.zip()
|> Zipper.find(fn
{:|>, _, [_, {{:., _, _}, _metadata, _} = func_node]} ->
inside?(func_node, position)

{{:., _, _}, _metadata, _} = node ->
inside?(node, position)

_ ->
false
end)

if result do
{:ok, Zipper.node(result)}
else
{:error, :not_found}
end
end

def find_params_index(ast, {line, column}) do
ast
|> Sourceror.get_args()
|> Enum.map(&Sourceror.get_meta/1)
|> Enum.find_index(fn meta ->
if meta[:closing] do
line <= meta[:closing][:line] and line >= meta[:line]
else
meta[:line] == line and column <= meta[:column]
end
end)
end

defp inside?(node, position) do
range = Sourceror.get_range(node)

Sourceror.compare_positions(range.start, position) == :lt &&
Sourceror.compare_positions(range.end, position) == :gt
end
end
end
83 changes: 83 additions & 0 deletions lib/next_ls/signature_help.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
defmodule NextLS.SignatureHelp do
@moduledoc false

alias GenLSP.Enumerations.MarkupKind
alias GenLSP.Structures.MarkupContent
alias GenLSP.Structures.ParameterInformation
alias GenLSP.Structures.SignatureHelp
alias GenLSP.Structures.SignatureInformation
alias NextLS.ASTHelpers

def fetch(text, position) do
ast =
text
|> Spitfire.parse(literal_encoder: &{:ok, {:__literal__, &2, [&1]}})
|> then(fn
{:ok, ast} -> ast
{:error, ast, _} -> ast
end)

with {:ok, result} <- ASTHelpers.Function.find_remote_function_call_within(ast, position) do
case result do
{:|>, _, [_, {{:., _, [{:__aliases__, _, modules}, name]}, _, _} = node]} ->
param_index = ASTHelpers.Function.find_params_index(node, position)

if param_index do
{:ok, {Module.concat(modules), name, param_index + 1}}
else
{:ok, {Module.concat(modules), name, nil}}
end

{{:., _, [{:__aliases__, _, modules}, name]}, _, _} = node ->
param_index = ASTHelpers.Function.find_params_index(node, position)

{:ok, {Module.concat(modules), name, param_index}}

_otherwise ->
{:error, :not_found}
end
end
end

def format({:ok, {:docs_v1, _, _lang, content_type, _, _, docs}}, func_name, param_index) do
for {{_, name, _arity}, _, [signature], fdoc, _} <- docs, name == func_name do
params_info =
signature
|> Spitfire.parse!()
|> Sourceror.get_args()
|> Enum.map(fn {name, _, _} ->
%ParameterInformation{
label: Atom.to_string(name)
}
end)

%SignatureHelp{
signatures: [
%SignatureInformation{
label: signature,
parameters: params_info,
documentation: maybe_doc(content_type, fdoc),
active_parameter: param_index
}
]
}
end
end

def format({:ok, {:error, :module_not_found}}, _func_name, _param_index) do
[]
end

def format({:error, :not_ready}, _func_name, _param_index) do
[]
end

defp maybe_doc(content_type, %{"en" => fdoc}) do
%MarkupContent{
kind: MarkupKind.markdown(),
value: NextLS.Docs.to_markdown(content_type, fdoc)
}
end

defp maybe_doc(_content_type, _fdoc), do: nil
end
Loading
Loading