Skip to content

Commit

Permalink
refactor(completions): improve cursor position detection (#444)
Browse files Browse the repository at this point in the history
* refactor(completions): improve cursor position detection

* fixup! refactor(completions): improve cursor position detection

* use completionItem/resolve to fetch docs

- this makes completion faster because we defer fetching docs
    - we binary_to_term/encode the relevant data to put in the data
      which then gets sent back up on the resolve request
- also includes signature in docs
- makes cursor finding faster by skipping subtrees that don't contain
  the cursor position

code is still WIP, which is why its ugly

on average of typing around in the next_ls.ex file, completions seem to
be around 45-55ms. still not as fast as I'd like them, but still
decent-ish

* improve docs handling

* more changes

* fix variables and work in test blocks

* stuff

* use container_cursor_to_quoted instead

* wip

* add support for <-

* expand the e in SomeError pattern in rescue clause

* remove old way of gather variables

* clean up

* fix dialyzer

* fix

* remove commented out code

* another

* fix tests

* fix test
  • Loading branch information
mhanberg committed May 8, 2024
1 parent 7f2f4f4 commit ea660ee
Show file tree
Hide file tree
Showing 15 changed files with 652 additions and 849 deletions.
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

0 comments on commit ea660ee

Please sign in to comment.