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

refactor(completions): improve cursor position detection #444

Merged
merged 19 commits into from
May 8, 2024
Merged
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
77 changes: 39 additions & 38 deletions .credo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#
included: [
"lib/",
"priv/monkey/",
"src/",
"test/",
"web/",
Expand Down Expand Up @@ -94,47 +95,47 @@
#
## Readability Checks
#
#{Credo.Check.Readability.AliasOrder, []},
#{Credo.Check.Readability.FunctionNames, []},
#{Credo.Check.Readability.LargeNumbers, []},
#{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
#{Credo.Check.Readability.ModuleAttributeNames, []},
#{Credo.Check.Readability.ModuleDoc, []},
#{Credo.Check.Readability.ModuleNames, []},
#{Credo.Check.Readability.ParenthesesInCondition, []},
#{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
#{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
#{Credo.Check.Readability.PredicateFunctionNames, []},
#{Credo.Check.Readability.PreferImplicitTry, []},
#{Credo.Check.Readability.RedundantBlankLines, []},
#{Credo.Check.Readability.Semicolons, []},
#{Credo.Check.Readability.SpaceAfterCommas, []},
#{Credo.Check.Readability.StringSigils, []},
#{Credo.Check.Readability.TrailingBlankLine, []},
#{Credo.Check.Readability.TrailingWhiteSpace, []},
#{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
#{Credo.Check.Readability.VariableNames, []},
#{Credo.Check.Readability.WithSingleClause, []},
# {Credo.Check.Readability.AliasOrder, []},
# {Credo.Check.Readability.FunctionNames, []},
# {Credo.Check.Readability.LargeNumbers, []},
# {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
# {Credo.Check.Readability.ModuleAttributeNames, []},
# {Credo.Check.Readability.ModuleDoc, []},
# {Credo.Check.Readability.ModuleNames, []},
# {Credo.Check.Readability.ParenthesesInCondition, []},
# {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
# {Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
# {Credo.Check.Readability.PredicateFunctionNames, []},
# {Credo.Check.Readability.PreferImplicitTry, []},
# {Credo.Check.Readability.RedundantBlankLines, []},
# {Credo.Check.Readability.Semicolons, []},
# {Credo.Check.Readability.SpaceAfterCommas, []},
# {Credo.Check.Readability.StringSigils, []},
# {Credo.Check.Readability.TrailingBlankLine, []},
# {Credo.Check.Readability.TrailingWhiteSpace, []},
# {Credo.Check.Readability.UnnecessaryAliasExpansion, []},
# {Credo.Check.Readability.VariableNames, []},
# {Credo.Check.Readability.WithSingleClause, []},

##
### Refactoring Opportunities
##
#{Credo.Check.Refactor.Apply, []},
#{Credo.Check.Refactor.CondStatements, []},
#{Credo.Check.Refactor.CyclomaticComplexity, []},
#{Credo.Check.Refactor.FilterCount, []},
#{Credo.Check.Refactor.FilterFilter, []},
#{Credo.Check.Refactor.FunctionArity, []},
#{Credo.Check.Refactor.LongQuoteBlocks, []},
#{Credo.Check.Refactor.MapJoin, []},
#{Credo.Check.Refactor.MatchInCondition, []},
#{Credo.Check.Refactor.NegatedConditionsInUnless, []},
#{Credo.Check.Refactor.NegatedConditionsWithElse, []},
#{Credo.Check.Refactor.Nesting, []},
#{Credo.Check.Refactor.RedundantWithClauseResult, []},
#{Credo.Check.Refactor.RejectReject, []},
#{Credo.Check.Refactor.UnlessWithElse, []},
#{Credo.Check.Refactor.WithClauses, []},
# {Credo.Check.Refactor.Apply, []},
# {Credo.Check.Refactor.CondStatements, []},
# {Credo.Check.Refactor.CyclomaticComplexity, []},
# {Credo.Check.Refactor.FilterCount, []},
# {Credo.Check.Refactor.FilterFilter, []},
# {Credo.Check.Refactor.FunctionArity, []},
# {Credo.Check.Refactor.LongQuoteBlocks, []},
# {Credo.Check.Refactor.MapJoin, []},
# {Credo.Check.Refactor.MatchInCondition, []},
# {Credo.Check.Refactor.NegatedConditionsInUnless, []},
# {Credo.Check.Refactor.NegatedConditionsWithElse, []},
# {Credo.Check.Refactor.Nesting, []},
# {Credo.Check.Refactor.RedundantWithClauseResult, []},
# {Credo.Check.Refactor.RejectReject, []},
# {Credo.Check.Refactor.UnlessWithElse, []},
# {Credo.Check.Refactor.WithClauses, []},

#
## Warnings
Expand All @@ -144,7 +145,7 @@
{Credo.Check.Warning.Dbg, []},
# {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
{Credo.Check.Warning.IExPry, []},
{Credo.Check.Warning.IoInspect, []},
{Credo.Check.Warning.IoInspect, []}
# {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
# {Credo.Check.Warning.OperationOnSameValues, []},
# {Credo.Check.Warning.OperationWithConstantResult, []},
Expand Down
3 changes: 2 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
assert_result: 3,
assert_notification: 3,
notify: 2,
request: 2
request: 2,
assert_match: 1
],
line_length: 120,
import_deps: [:gen_lsp],
Expand Down
186 changes: 95 additions & 91 deletions lib/next_ls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ defmodule NextLS do
{:ok,
assign(lsp,
auto_update: Keyword.get(args, :auto_update, false),
bundle_base: bundle_base,
exit_code: 1,
documents: %{},
refresh_refs: %{},
Expand Down Expand Up @@ -144,7 +145,8 @@ defmodule NextLS do
completion_provider:
if init_opts.experimental.completions.enable do
%GenLSP.Structures.CompletionOptions{
trigger_characters: [".", "@", "&", "%", "^", ":", "!", "-", "~", "/", "{"]
trigger_characters: [".", "@", "&", "%", "^", ":", "!", "-", "~", "/", "{"],
resolve_provider: true
}
else
nil
Expand Down Expand Up @@ -401,32 +403,16 @@ defmodule NextLS do
end)

value =
with {:ok, {:docs_v1, _, _lang, content_type, %{"en" => mod_doc}, _, fdocs}} <- result do
with {:ok, result} <- result,
%NextLS.Docs{} = doc <- NextLS.Docs.new(result, mod) do
case reference.type do
"alias" ->
"""
## #{reference.module}

#{NextLS.HoverHelpers.to_markdown(content_type, mod_doc)}
"""
NextLS.Docs.module(doc)

"function" ->
doc =
Enum.find(fdocs, fn {{type, name, _a}, _, _, _doc, _} ->
type in [:function, :macro] and to_string(name) == reference.identifier
end)

case doc do
{_, _, _, %{"en" => fdoc}, _} ->
"""
## #{Macro.to_string(mod)}.#{reference.identifier}/#{reference.arity}

#{NextLS.HoverHelpers.to_markdown(content_type, fdoc)}
"""

_ ->
nil
end
NextLS.Docs.function(doc, fn name, a, documentation, _other ->
to_string(name) == reference.identifier and documentation != :hidden and a >= reference.arity
end)

_ ->
nil
Expand Down Expand Up @@ -583,47 +569,49 @@ defmodule NextLS do
resp
end

def handle_request(%TextDocumentCompletion{params: %{text_document: %{uri: uri}, position: position}}, lsp) do
document = lsp.assigns.documents[uri]
def handle_request(%GenLSP.Requests.CompletionItemResolve{params: completion_item}, lsp) do
completion_item =
with nil <- completion_item.data do
completion_item
else
%{"uri" => uri, "data" => data} ->
data = data |> Base.decode64!() |> :erlang.binary_to_term()

spliced =
document
|> List.update_at(position.line, fn row ->
{front, back} = String.split_at(row, position.character)
# all we need to do is insert the cursor so we can find the spot to then
# calculate the environment, it doens't really matter if its valid code,
# it probably isn't already
front <> "\n__cursor__()\n" <> back
end)
|> Enum.join("\n")
module =
case data do
{mod, _function, _arity} -> mod
mod -> mod
end

ast =
spliced
|> Spitfire.parse(literal_encoder: &{:ok, {:__block__, &2, [&1]}})
|> then(fn
{:ok, ast} -> ast
{:error, ast, _} -> ast
{:error, :no_fuel_remaining} -> nil
end)
result =
dispatch_to_workspace(lsp.assigns.registry, uri, fn runtime, _wuri ->
Runtime.call(runtime, {Code, :fetch_docs, [module]})
end)

env =
ast
|> NextLS.ASTHelpers.find_cursor()
|> then(fn
{:ok, cursor} ->
cursor
docs =
with {:ok, doc} <- result,
%NextLS.Docs{} = doc <- NextLS.Docs.new(doc, module) do
case data do
{_mod, function, arity} ->
NextLS.Docs.function(doc, fn name, a, documentation, _other ->
to_string(name) == function and documentation != :hidden and a >= arity
end)

{:error, :not_found} ->
NextLS.Logger.warning(lsp.assigns.logger, "Could not locate cursor when building environment")
mod when is_atom(mod) ->
NextLS.Docs.module(doc)
end
else
_ -> nil
end

NextLS.Logger.warning(
lsp.assigns.logger,
"Source code that produced the above warning: #{spliced}"
)
%{completion_item | documentation: docs}
end

nil
end)
|> NextLS.ASTHelpers.Env.build()
{:reply, completion_item, lsp}
end

def handle_request(%TextDocumentCompletion{params: %{text_document: %{uri: uri}, position: position}}, lsp) do
document = lsp.assigns.documents[uri]

document_slice =
document
Expand All @@ -636,34 +624,23 @@ defmodule NextLS do
|> Enum.reverse()
|> Enum.join("\n")

with_cursor =
case Spitfire.container_cursor_to_quoted(document_slice) do
{:ok, with_cursor} -> with_cursor
{:error, with_cursor, _} -> with_cursor
end

{root_path, entries} =
dispatch(lsp.assigns.registry, :runtimes, fn entries ->
[{wuri, result}] =
for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do
ast =
spliced
|> Spitfire.parse()
|> then(fn
{:ok, ast} -> ast
{:error, ast, _} -> ast
{:error, :no_fuel_remaining} -> nil
end)

{:ok, {_, _, _, macro_env}} = Runtime.expand(runtime, ast, Path.basename(uri))

env =
env
|> Map.put(:functions, macro_env.functions)
|> Map.put(:macros, macro_env.macros)
|> Map.put(:aliases, macro_env.aliases)
|> Map.put(:attrs, macro_env.attrs)

{wuri,
document_slice
|> String.to_charlist()
|> Enum.reverse()
|> NextLS.Autocomplete.expand(runtime, env)}
end
dispatch_to_workspace(lsp.assigns.registry, uri, fn runtime, wuri ->
{:ok, {_, _, _, macro_env}} =
Runtime.expand(runtime, with_cursor, Path.basename(uri))

doc =
document_slice
|> String.to_charlist()
|> Enum.reverse()

result = NextLS.Autocomplete.expand(doc, runtime, macro_env)

case result do
{:yes, entries} -> {wuri, entries}
Expand All @@ -680,13 +657,13 @@ defmodule NextLS do
{name, GenLSP.Enumerations.CompletionItemKind.struct(), ""}

:function ->
{"#{name}/#{symbol.arity}", GenLSP.Enumerations.CompletionItemKind.function(), symbol.docs}
{"#{name}/#{symbol.arity}", GenLSP.Enumerations.CompletionItemKind.function(), symbol[:docs] || ""}

:module ->
{name, GenLSP.Enumerations.CompletionItemKind.module(), symbol.docs}
{name, GenLSP.Enumerations.CompletionItemKind.module(), symbol[:docs] || ""}

:variable ->
{name, GenLSP.Enumerations.CompletionItemKind.variable(), ""}
{to_string(name), GenLSP.Enumerations.CompletionItemKind.variable(), ""}

:dir ->
{name, GenLSP.Enumerations.CompletionItemKind.folder(), ""}
Expand All @@ -711,8 +688,14 @@ defmodule NextLS do
%GenLSP.Structures.CompletionItem{
label: label,
kind: kind,
insert_text: name,
documentation: docs
insert_text: to_string(name),
documentation: docs,
data:
if symbol[:data] do
%{uri: uri, data: symbol[:data] |> :erlang.term_to_binary() |> Base.encode64()}
else
nil
end
}

root_path = root_path |> URI.parse() |> Map.get(:path)
Expand Down Expand Up @@ -894,7 +877,7 @@ defmodule NextLS do
lsp.assigns.init_opts.elixir_bin_path

lsp.assigns.init_opts.experimental.completions.enable ->
NextLS.Runtime.BundledElixir.binpath()
NextLS.Runtime.BundledElixir.binpath(lsp.assigns.bundle_base)

true ->
"elixir" |> System.find_executable() |> Path.dirname()
Expand Down Expand Up @@ -1345,6 +1328,27 @@ defmodule NextLS do
end
end

defp dispatch_to_workspace(registry, uri, callback) do
ref = make_ref()
me = self()

Registry.dispatch(registry, :runtimes, fn entries ->
[result] =
for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do
callback.(runtime, wuri)
end

send(me, {ref, result})
end)

receive do
{^ref, result} -> result
after
1000 ->
:timeout
end
end

defp symbol_info(file, line, col, database) do
definition_query = ~Q"""
SELECT module, type, name
Expand Down
Loading
Loading