diff --git a/.credo.exs b/.credo.exs index c189252a..98886e41 100644 --- a/.credo.exs +++ b/.credo.exs @@ -23,6 +23,7 @@ # included: [ "lib/", + "priv/monkey/", "src/", "test/", "web/", @@ -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 @@ -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, []}, diff --git a/.formatter.exs b/.formatter.exs index ada3d7cd..19ef713c 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -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], diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 4a5cc898..9d2931fb 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -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: %{}, @@ -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 @@ -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 @@ -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 @@ -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} @@ -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(), ""} @@ -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) @@ -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() @@ -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 diff --git a/lib/next_ls/autocomplete.ex b/lib/next_ls/autocomplete.ex index 742ded29..dd9fb3e8 100644 --- a/lib/next_ls/autocomplete.ex +++ b/lib/next_ls/autocomplete.ex @@ -2,6 +2,7 @@ defmodule NextLS.Autocomplete do # Based on `IEx.Autocomplete` from github.com/elixir-lang/elixir from 10/17/2023 @moduledoc false + require Logger require NextLS.Runtime @bitstring_modifiers [ @@ -13,8 +14,8 @@ defmodule NextLS.Autocomplete do %{kind: :variable, name: "little"}, %{kind: :variable, name: "native"}, %{kind: :variable, name: "signed"}, - %{kind: :function, name: "size", arity: 1, docs: nil}, - %{kind: :function, name: "unit", arity: 1, docs: nil}, + %{kind: :function, name: "size", arity: 1}, + %{kind: :function, name: "unit", arity: 1}, %{kind: :variable, name: "unsigned"}, %{kind: :variable, name: "utf8"}, %{kind: :variable, name: "utf16"}, @@ -33,7 +34,6 @@ defmodule NextLS.Autocomplete do defp expand_code(code, runtime, env) do code = Enum.reverse(code) - # helper = get_helper(code) case Code.Fragment.cursor_context(code) do {:alias, alias} -> @@ -164,37 +164,20 @@ defmodule NextLS.Autocomplete do ## Expand call - defp expand_local_call(fun, runtime, env) do + defp expand_local_call(fun, _runtime, env) do env |> imports_from_env() |> Enum.filter(fn {_, funs} -> List.keymember?(funs, fun, 0) end) - |> Enum.flat_map(fn {module, _} -> get_signatures(fun, module) end) - |> expand_signatures(runtime, env) + |> format_expansion() end - defp expand_dot_call(path, fun, runtime, env) do + defp expand_dot_call(path, fun, _runtime, env) do case expand_dot_path(path, env) do - {:ok, mod} when is_atom(mod) -> fun |> get_signatures(mod) |> expand_signatures(runtime) + {:ok, mod} when is_atom(mod) -> format_expansion(fun) _ -> no() end end - defp get_signatures(name, module) when is_atom(module) do - with docs when is_list(docs) <- get_docs(module, [:function, :macro], name) do - Enum.map(docs, fn {_, _, signatures, _, _} -> Enum.join(signatures, " ") end) - else - _ -> [] - end - end - - defp expand_signatures([_ | _] = signatures, _runtime) do - [head | _tail] = Enum.sort(signatures, &(String.length(&1) <= String.length(&2))) - # if tail != [], do: IO.write("\n" <> (tail |> Enum.reverse() |> Enum.join("\n"))) - yes([head]) - end - - defp expand_signatures([], runtime, env), do: expand_local_or_var("", "", runtime, env) - ## Expand dot defp expand_dot(path, hint, exact?, runtime, env) do @@ -291,7 +274,7 @@ defmodule NextLS.Autocomplete do defp match_var(code, hint, _runtime, env) do code |> variables_from_binding(env) - |> Enum.filter(&String.starts_with?(&1, hint)) + |> Enum.filter(&String.starts_with?(to_string(&1), hint)) |> Enum.sort() |> Enum.map(&%{kind: :variable, name: &1}) end @@ -304,23 +287,10 @@ defmodule NextLS.Autocomplete do defp match_erlang_modules(hint, runtime) do for mod <- match_modules(hint, false, runtime), usable_as_unquoted_module?(mod) do - {content_type, mdoc} = - case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(mod)) do - {:ok, {:docs_v1, _, _lang, content_type, %{"en" => mdoc}, _, _fdocs}} -> - {content_type, mdoc} - - _ -> - {"text/markdown", nil} - end - %{ kind: :module, name: mod, - docs: """ - ## #{Macro.to_string(mod)} - - #{NextLS.HoverHelpers.to_markdown(content_type, mdoc)} - """ + data: String.to_atom(mod) } end end @@ -512,28 +482,15 @@ defmodule NextLS.Autocomplete do end end - defp match_aliases(hint, runtime, env) do + defp match_aliases(hint, _runtime, env) do for {alias, module} <- aliases_from_env(env), [name] = Module.split(alias), String.starts_with?(name, hint) do - {content_type, mdoc} = - case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(module)) do - {:ok, {:docs_v1, _, _lang, content_type, %{"en" => mdoc}, _, _fdocs}} -> - {content_type, mdoc} - - _ -> - {"text/markdown", nil} - end - %{ kind: :module, name: name, - module: module, - docs: """ - ## #{Macro.to_string(module)} - - #{NextLS.HoverHelpers.to_markdown(content_type, mdoc)} - """ + data: module, + module: module } end end @@ -551,23 +508,10 @@ defmodule NextLS.Autocomplete do valid_alias_piece?("." <> name) do alias = Module.concat([mod]) - {content_type, mdoc} = - case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(alias)) do - {:ok, {:docs_v1, _, _lang, content_type, %{"en" => mdoc}, _, _fdocs}} -> - {content_type, mdoc} - - _ -> - {"text/markdown", nil} - end - %{ kind: :module, - name: name, - docs: """ - ## #{Macro.to_string(alias)} - - #{NextLS.HoverHelpers.to_markdown(content_type, mdoc)} - """ + data: alias, + name: name } end @@ -637,10 +581,7 @@ defmodule NextLS.Autocomplete do end defp get_modules(false, runtime) do - {:ok, mods} = - NextLS.Runtime.execute runtime do - :code.all_loaded() - end + mods = NextLS.Runtime.execute!(runtime, do: :code.all_loaded()) modules = Enum.map(mods, &Atom.to_string(elem(&1, 0))) @@ -678,43 +619,16 @@ defmodule NextLS.Autocomplete do apps end - defp match_module_funs(runtime, mod, funs, hint, exact?) do - {content_type, fdocs} = - case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(mod)) do - {:ok, {:docs_v1, _, _lang, content_type, _, _, fdocs}} -> - {content_type, fdocs} - - _ -> - {"text/markdown", []} - end - + defp match_module_funs(_runtime, mod, funs, hint, exact?) do functions = for {fun, arity} <- funs, name = Atom.to_string(fun), if(exact?, do: name == hint, else: String.starts_with?(name, hint)) do - doc = - Enum.find(fdocs, fn {{type, fname, _a}, _, _, _doc, _} -> - type in [:function, :macro] and to_string(fname) == name - end) - - doc = - case doc do - {_, _, _, %{"en" => fdoc}, _} -> - """ - ## #{Macro.to_string(mod)}.#{name}/#{arity} - - #{NextLS.HoverHelpers.to_markdown(content_type, fdoc)} - """ - - _ -> - nil - end - %{ kind: :function, + data: {mod, name, arity}, name: name, - arity: arity, - docs: doc + arity: arity } end @@ -737,12 +651,6 @@ defmodule NextLS.Autocomplete do not ensure_loaded?(mod, runtime) -> [] - docs = get_docs(mod, [:function, :macro]) -> - mod - |> exports(runtime) - |> Kernel.--(default_arg_functions_with_doc_false(docs)) - |> Enum.reject(&hidden_fun?(&1, docs)) - true -> exports(mod, runtime) end @@ -783,34 +691,6 @@ defmodule NextLS.Autocomplete do # end # end - defp get_docs(mod, kinds, fun \\ nil) do - case Code.fetch_docs(mod) do - {:docs_v1, _, _, _, _, _, docs} -> - if is_nil(fun) do - for {{kind, _, _}, _, _, _, _} = doc <- docs, kind in kinds, do: doc - else - for {{kind, ^fun, _}, _, _, _, _} = doc <- docs, kind in kinds, do: doc - end - - {:error, _} -> - nil - end - end - - defp default_arg_functions_with_doc_false(docs) do - for {{_, fun_name, arity}, _, _, :hidden, %{defaults: count}} <- docs, - new_arity <- (arity - count)..arity, - do: {fun_name, new_arity} - end - - defp hidden_fun?({name, arity}, docs) do - case Enum.find(docs, &match?({{_, ^name, ^arity}, _, _, _, _}, &1)) do - nil -> hd(Atom.to_charlist(name)) == ?_ - {_, _, _, :hidden, _} -> true - {_, _, _, _, _} -> false - end - end - defp ensure_loaded?(Elixir, _runtime), do: false defp ensure_loaded?(mod, runtime) do @@ -833,11 +713,6 @@ defmodule NextLS.Autocomplete do end defp value_from_binding([_var | _path], _runtime) do - # with {evaluator, server} <- IEx.Broker.evaluator(runtime) do - # IEx.Evaluator.value_from_binding(evaluator, server, var, path) - # else - # _ -> :error - # end [] end diff --git a/lib/next_ls/docs.ex b/lib/next_ls/docs.ex new file mode 100644 index 00000000..8359d840 --- /dev/null +++ b/lib/next_ls/docs.ex @@ -0,0 +1,154 @@ +defmodule NextLS.Docs do + @moduledoc false + + defstruct module: nil, mdoc: nil, functions: [], content_type: nil + + def new({:docs_v1, _, _lang, content_type, mdoc, _, fdocs}, module) do + mdoc = + case mdoc do + %{"en" => mdoc} -> mdoc + _ -> nil + end + + %__MODULE__{ + content_type: content_type, + module: module, + mdoc: mdoc, + functions: fdocs + } + end + + def new(_, _) do + nil + end + + def module(%__MODULE__{} = doc) do + """ + ## #{Macro.to_string(doc.module)} + + #{to_markdown(doc.content_type, doc.mdoc)} + """ + end + + def function(%__MODULE__{} = doc, callback) do + result = + Enum.find(doc.functions, fn {{type, name, arity}, _some_number, _signature, doc, other} -> + type in [:function, :macro] and callback.(name, arity, doc, other) + end) + + case result do + {{_, name, arity}, _some_number, signature, %{"en" => fdoc}, _other} -> + """ + ## #{Macro.to_string(doc.module)}.#{name}/#{arity} + + `#{signature}` + + #{to_markdown(doc.content_type, fdoc)} + """ + + _ -> + nil + end + end + + @spec to_markdown(String.t(), String.t() | list()) :: String.t() + def to_markdown(type, docs) + def to_markdown("text/markdown", docs), do: docs + + def to_markdown("application/erlang+html" = type, [{:p, _, children} | rest]) do + String.trim(to_markdown(type, children) <> "\n\n" <> to_markdown(type, rest)) + end + + def to_markdown("application/erlang+html" = type, [{:div, attrs, children} | rest]) do + prefix = + if attrs[:class] in ~w do + "> " + else + "" + end + + prefix <> to_markdown(type, children) <> "\n\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:a, attrs, children} | rest]) do + space = if List.last(children) == " ", do: " ", else: "" + + "[#{String.trim(to_markdown(type, children))}](#{attrs[:href]})" <> space <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [doc | rest]) when is_binary(doc) do + doc <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:h1, _, children} | rest]) do + "# #{to_markdown(type, children)}\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:h2, _, children} | rest]) do + "## #{to_markdown(type, children)}\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:h3, _, children} | rest]) do + "### #{to_markdown(type, children)}\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:h4, _, children} | rest]) do + "#### #{to_markdown(type, children)}\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:h5, _, children} | rest]) do + "##### #{to_markdown(type, children)}\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:pre, _, [{:code, _, children}]} | rest]) do + "```erlang\n#{to_markdown(type, children)}\n```\n\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:ul, [class: "types"], lis} | rest]) do + "### Types\n\n#{to_markdown(type, lis)}\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:ul, _, lis} | rest]) do + "#{to_markdown(type, lis)}\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:li, [name: text], _} | rest]) do + "* #{text}\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:li, _, children} | rest]) do + "* #{to_markdown(type, children)}\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:code, _, bins} | rest]) do + "`#{IO.iodata_to_binary(bins)}`" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:em, _, bins} | rest]) do + "_#{IO.iodata_to_binary(bins)}_" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:dl, _, lis} | rest]) do + "#{to_markdown(type, lis)}\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:dt, _, children} | rest]) do + "* #{to_markdown(type, children)}\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:dd, _, children} | rest]) do + "#{to_markdown(type, children)}\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:i, _, children} | rest]) do + "_#{IO.iodata_to_binary(children)}_" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html", []) do + "" + end + + def to_markdown("application/erlang+html", nil) do + "" + end +end diff --git a/lib/next_ls/helpers/ast_helpers.ex b/lib/next_ls/helpers/ast_helpers.ex index 5f78c464..7b5b9981 100644 --- a/lib/next_ls/helpers/ast_helpers.ex +++ b/lib/next_ls/helpers/ast_helpers.ex @@ -170,7 +170,7 @@ defmodule NextLS.ASTHelpers do |> Zipper.zip() |> Zipper.traverse_while(nil, fn tree, acc -> node = Zipper.node(tree) - node_range = Sourceror.get_range(node) + node_range = Sourceror.Range.get_range(node) is_inside = with nil <- node_range do @@ -204,22 +204,6 @@ defmodule NextLS.ASTHelpers do end end - def find_cursor(ast) do - with nil <- - ast - |> Zipper.zip() - |> Zipper.find(fn - {:@, _, [{:__cursor__, _, []}]} -> true - {:__cursor__, _, _} -> true - {{:., _, [_, :__cursor__]}, _, _} -> true - _ -> false - end) do - {:error, :not_found} - else - zipper -> {:ok, zipper} - end - end - def top(nil, acc, _callback), do: acc def top(%Zipper{path: nil} = zipper, acc, callback), do: callback.(Zipper.node(zipper), zipper, acc) diff --git a/lib/next_ls/helpers/ast_helpers/env.ex b/lib/next_ls/helpers/ast_helpers/env.ex deleted file mode 100644 index d1cc376b..00000000 --- a/lib/next_ls/helpers/ast_helpers/env.ex +++ /dev/null @@ -1,141 +0,0 @@ -defmodule NextLS.ASTHelpers.Env do - @moduledoc false - alias Sourceror.Zipper - - defp inside?(range, position) do - Sourceror.compare_positions(range.start, position) in [:lt, :eq] && - Sourceror.compare_positions(range.end, position) in [:gt, :eq] - end - - def build(nil) do - %{variables: []} - end - - def build(cursor) do - position = cursor |> Zipper.node() |> Sourceror.get_range() |> Map.get(:start) - zipper = Zipper.prev(cursor) - - env = - ascend(zipper, %{variables: [], attrs: []}, fn node, zipper, acc -> - is_inside = - with {_, _, _} <- node, - range when not is_nil(range) <- Sourceror.get_range(node) do - inside?(range, position) - else - _ -> - false - end - - case node do - {match_op, _, [pm | _]} when match_op in [:=] and not is_inside -> - {_, vars} = - Macro.prewalk(pm, [], fn node, acc -> - case node do - {name, _, nil} -> - {node, [to_string(name) | acc]} - - _ -> - {node, acc} - end - end) - - Map.update!(acc, :variables, &(vars ++ &1)) - - {match_op, _, [pm | rhs]} when match_op in [:<-] -> - up_node = zipper |> Zipper.up() |> Zipper.node() - - # in_match operator comes with for and with normally, so we need to - # check if we are inside the parent node, which is the for/with - is_inside_p = - with {_, _, _} <- up_node, range when not is_nil(range) <- Sourceror.get_range(up_node) do - inside?(range, position) - else - _ -> - false - end - - is_inside_rhs = - with range when not is_nil(range) <- Sourceror.get_range(rhs) do - inside?(range, position) - else - _ -> - false - end - - if is_inside_p and not is_inside_rhs do - {_, vars} = - Macro.prewalk(pm, [], fn node, acc -> - case node do - {name, _, nil} -> - {node, [to_string(name) | acc]} - - _ -> - {node, acc} - end - end) - - Map.update!(acc, :variables, &(vars ++ &1)) - else - acc - end - - {def, _, [{_, _, args} | _]} - when def in [:def, :defp, :defmacro, :defmacrop] and args != [] and is_inside -> - {_, vars} = - Macro.prewalk(args, [], fn node, acc -> - case node do - {name, _, nil} -> - {node, [to_string(name) | acc]} - - _ -> - {node, acc} - end - end) - - Map.update!(acc, :variables, &(vars ++ &1)) - - {:->, _, [args | _]} when args != [] -> - {_, vars} = - Macro.prewalk(args, [], fn node, acc -> - case node do - {name, _, nil} -> - {node, [to_string(name) | acc]} - - _ -> - {node, acc} - end - end) - - Map.update!(acc, :variables, &(vars ++ &1)) - - _ -> - acc - end - end) - - %{ - variables: Enum.uniq(env.variables) - } - end - - def ascend(nil, acc, _callback), do: acc - - def ascend(%Zipper{path: nil} = zipper, acc, callback), do: callback.(Zipper.node(zipper), zipper, acc) - - def ascend(zipper, acc, callback) do - node = Zipper.node(zipper) - acc = callback.(node, zipper, acc) - - zipper = - cond do - match?({:->, _, _}, node) -> - Zipper.up(zipper) - - true -> - left = Zipper.left(zipper) - if left, do: left, else: Zipper.up(zipper) - end - - ascend(zipper, acc, callback) - end -end diff --git a/lib/next_ls/helpers/hover_helpers.ex b/lib/next_ls/helpers/hover_helpers.ex deleted file mode 100644 index efe4d4e8..00000000 --- a/lib/next_ls/helpers/hover_helpers.ex +++ /dev/null @@ -1,68 +0,0 @@ -defmodule NextLS.HoverHelpers do - @moduledoc false - - @spec to_markdown(String.t(), String.t() | list()) :: String.t() - def to_markdown(type, docs) - def to_markdown("text/markdown", docs), do: docs - - def to_markdown("application/erlang+html" = type, [{:p, _, children} | rest]) do - String.trim(to_markdown(type, children) <> "\n\n" <> to_markdown(type, rest)) - end - - def to_markdown("application/erlang+html" = type, [{:div, attrs, children} | rest]) do - prefix = - if attrs[:class] in ~w do - "> " - else - "" - end - - prefix <> to_markdown(type, children) <> "\n\n" <> to_markdown(type, rest) - end - - def to_markdown("application/erlang+html" = type, [{:a, attrs, children} | rest]) do - space = if List.last(children) == " ", do: " ", else: "" - - "[#{String.trim(to_markdown(type, children))}](#{attrs[:href]})" <> space <> to_markdown(type, rest) - end - - def to_markdown("application/erlang+html" = type, [doc | rest]) when is_binary(doc) do - doc <> to_markdown(type, rest) - end - - def to_markdown("application/erlang+html" = type, [{:pre, _, [{:code, _, children}]} | rest]) do - "```erlang\n#{to_markdown(type, children)}\n```\n\n" <> to_markdown(type, rest) - end - - def to_markdown("application/erlang+html" = type, [{:ul, _, lis} | rest]) do - "#{to_markdown(type, lis)}\n" <> to_markdown(type, rest) - end - - def to_markdown("application/erlang+html" = type, [{:li, _, children} | rest]) do - "* #{to_markdown(type, children)}\n" <> to_markdown(type, rest) - end - - def to_markdown("application/erlang+html" = type, [{:code, _, bins} | rest]) do - "`#{IO.iodata_to_binary(bins)}`" <> to_markdown(type, rest) - end - - def to_markdown("application/erlang+html" = type, [{:em, _, bins} | rest]) do - "_#{IO.iodata_to_binary(bins)}_" <> to_markdown(type, rest) - end - - def to_markdown("application/erlang+html" = type, [{:dl, _, lis} | rest]) do - "#{to_markdown(type, lis)}\n" <> to_markdown(type, rest) - end - - def to_markdown("application/erlang+html" = type, [{:dt, _, children} | rest]) do - "* #{to_markdown(type, children)}\n" <> to_markdown(type, rest) - end - - def to_markdown("application/erlang+html" = type, [{:dd, _, children} | rest]) do - "#{to_markdown(type, children)}\n" <> to_markdown(type, rest) - end - - def to_markdown("application/erlang+html", []) do - "" - end -end diff --git a/mix.lock b/mix.lock index bd9d3256..cfd59813 100644 --- a/mix.lock +++ b/mix.lock @@ -46,8 +46,8 @@ "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "req": {:hex, :req, "0.4.0", "1c759054dd64ef1b1a0e475c2d2543250d18f08395d3174c371b7746984579ce", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "f53eadc32ebefd3e5d50390356ec3a59ed2b8513f7da8c6c3f2e14040e9fe989"}, "schematic": {:hex, :schematic, "0.2.1", "0b091df94146fd15a0a343d1bd179a6c5a58562527746dadd09477311698dbb1", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0b255d65921e38006138201cd4263fd8bb807d9dfc511074615cd264a571b3b1"}, - "sourceror": {:hex, :sourceror, "1.0.2", "c5e86fdc14881f797749d1fe5df017ca66727a8146e7ee3e736605a3df78f3e6", [:mix], [], "hexpm", "832335e87d0913658f129d58b2a7dc0490ddd4487b02de6d85bca0169ec2bd79"}, - "spitfire": {:git, "https://github.com/elixir-tools/spitfire.git", "f913c6025875c9d69b4d35f94cae3e70c7f6320e", []}, + "sourceror": {:hex, :sourceror, "1.0.3", "111711c147f4f1414c07a67b45ad0064a7a41569037355407eda635649507f1d", [:mix], [], "hexpm", "56c21ef146c00b51bc3bb78d1f047cb732d193256a7c4ba91eaf828d3ae826af"}, + "spitfire": {:git, "https://github.com/elixir-tools/spitfire.git", "e47385f64db19f65b8efdd57d003272376446a4e", []}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "styler": {:hex, :styler, "0.8.1", "f3c0f65023e4bfbf7e7aa752d128b8475fdabfd30f96ee7314b84480cc56e788", [:mix], [], "hexpm", "1aa48d3aa689a639289af3d8254d40e068e98c083d6e5e3d1a695e71a147b344"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, diff --git a/priv/monkey/_next_ls_private_compiler.ex b/priv/monkey/_next_ls_private_compiler.ex index da8dafb1..407977d6 100644 --- a/priv/monkey/_next_ls_private_compiler.ex +++ b/priv/monkey/_next_ls_private_compiler.ex @@ -1116,7 +1116,8 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do cursor_env.functions, macros: Enum.filter(Map.get(state, :macros, []), fn {m, _} -> m == cursor_env.module end) ++ cursor_env.macros, - attrs: Map.get(cursor_state, :attrs, []) + attrs: Enum.uniq(Map.get(cursor_state, :attrs, [])), + variables: for({name, nil} <- cursor_env.versioned_vars, do: name) } ) @@ -1128,11 +1129,6 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do {node, state, env} end - defp expand({{:., _, [_, :__cursor__]}, _, _} = node, state, env) do - Process.put(:cursor_env, {state, env}) - {node, state, env} - end - defp expand({:@, _, [{:__cursor__, _, _}]} = node, state, env) do Process.put(:cursor_env, {state, env}) {node, state, env} @@ -1266,10 +1262,10 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do # you would collect this information in expand_pattern/3 and # invoke it from all relevant places (such as case, cond, try, etc). - defp expand({:=, meta, [left, right]}, state, env) do + defp expand({match, meta, [left, right]}, state, env) when match in [:=, :<-] do {left, state, env} = expand_pattern(left, state, env) {right, state, env} = expand(right, state, env) - {{:=, meta, [left, right]}, state, env} + {{match, meta, [left, right]}, state, env} end ## quote/1, quote/2 @@ -1297,6 +1293,17 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do {{:^, meta, [arg]}, state, %{env | context: context}} end + defp expand({:->, _, [params, block]} = ast, state, env) do + {_, state, penv} = + for p <- params, reduce: {nil, state, env} do + {_, state, penv} -> + expand_pattern(p, state, penv) + end + + {res, state, _env} = expand(block, state, penv) + {res, state, env} + end + ## Remote call defp expand({{:., dot_meta, [module, fun]}, meta, args}, state, env) when is_atom(fun) and is_list(args) do @@ -1319,6 +1326,18 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do end end + # self calling anonymous function + + defp expand({{:., dmeta, [func]}, callmeta, args}, state, env) when is_list(args) do + {res, state, _env} = expand(func, state, env) + {res, state, env} + end + + defp expand({:in, meta, [left, right]} = ast, state, %{context: :match} = env) do + {left, state, env} = expand_pattern(left, state, env) + {{:in, meta, [left, right]}, state, env} + end + ## Imported or local call defp expand({fun, meta, args}, state, env) when is_atom(fun) and is_list(args) do @@ -1365,11 +1384,12 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do # For the language server, we only want to capture definitions, # we don't care when they are used. - # defp expand({var, meta, ctx} = ast, state, %{context: :match} = env) when is_atom(var) and is_atom(ctx) do - # ctx = Keyword.get(meta, :context, ctx) - # # state = update_in(state.vars, &[{var, ctx} | &1]) - # {ast, state, env} - # end + defp expand({var, meta, ctx} = ast, state, %{context: :match} = env) when is_atom(var) and is_atom(ctx) do + ctx = Keyword.get(meta, :context, ctx) + vv = Map.update(env.versioned_vars, var, ctx, fn _ -> ctx end) + + {ast, state, %{env | versioned_vars: vv}} + end ## Fallback @@ -1384,7 +1404,7 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do # definition, fully replacing the actual implementation. You could also # use this to capture module attributes (optionally delegating to the actual # implementation), function expansion, and more. - defp expand_macro(_meta, Kernel, :defmodule, [alias, [do: block]], _callback, state, env) do + defp expand_macro(_meta, Kernel, :defmodule, [alias, [{_, block}]], _callback, state, env) do {expanded, state, env} = expand(alias, state, env) if is_atom(expanded) do @@ -1405,9 +1425,15 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do end end - defp expand_macro(_meta, Kernel, type, [{name, _, params}, block], _callback, state, env) + defp expand_macro(_meta, Kernel, type, [{name, _, params}, [{_, block}]], _callback, state, env) when type in [:def, :defp] and is_tuple(block) and is_atom(name) and is_list(params) do - {res, state, _env} = expand(block, state, env) + {_, state, penv} = + for p <- params, reduce: {nil, state, env} do + {_, state, penv} -> + expand_pattern(p, state, penv) + end + + {res, state, _env} = expand(block, state, penv) arity = length(List.wrap(params)) functions = Map.update(state.functions, env.module, [{name, arity}], &Keyword.put_new(&1, name, arity)) @@ -1416,7 +1442,8 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do defp expand_macro(_meta, Kernel, type, [{name, _, params}, block], _callback, state, env) when type in [:defmacro, :defmacrop] do - {res, state, _env} = expand(block, state, env) + {_res, state, penv} = expand(params, state, env) + {res, state, _env} = expand(block, state, penv) arity = length(List.wrap(params)) macros = Map.update(state.macros, env.module, [{name, arity}], &Keyword.put_new(&1, name, arity)) @@ -1425,10 +1452,16 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do defp expand_macro(_meta, Kernel, type, [{name, _, params}, blocks], _callback, state, env) when type in [:def, :defp] and is_atom(name) and is_list(params) and is_list(blocks) do + {_, state, penv} = + for p <- params, reduce: {nil, state, env} do + {_, state, penv} -> + expand_pattern(p, state, penv) + end + {blocks, state} = for {type, block} <- blocks, reduce: {[], state} do {acc, state} -> - {res, state, _env} = expand(block, state, env) + {res, state, _env} = expand(block, state, penv) {[{type, res} | acc], state} end @@ -1438,12 +1471,18 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do {Enum.reverse(blocks), put_in(state.functions, functions), env} end - defp expand_macro(_meta, Kernel, type, [{_name, _, params}, blocks], _callback, state, env) + defp expand_macro(_meta, Kernel, type, [{name, _, params}, blocks], _callback, state, env) when type in [:def, :defp] and is_list(params) and is_list(blocks) do + {_, state, penv} = + for p <- params, reduce: {nil, state, env} do + {_, state, penv} -> + expand_pattern(p, state, penv) + end + {blocks, state} = for {type, block} <- blocks, reduce: {[], state} do {acc, state} -> - {res, state, _env} = expand(block, state, env) + {res, state, _env} = expand(block, state, penv) {[{type, res} | acc], state} end diff --git a/test/next_ls/autocomplete_test.exs b/test/next_ls/autocomplete_test.exs index 048775f7..54dd20ed 100644 --- a/test/next_ls/autocomplete_test.exs +++ b/test/next_ls/autocomplete_test.exs @@ -95,7 +95,7 @@ defmodule NextLS.AutocompleteTest do end test "Erlang module completion", %{runtime: runtime} do - assert expand(runtime, ~c":zl") == {:yes, [%{name: "zlib", kind: :module, docs: "## \"zlib\"\n\n\n"}]} + assert expand(runtime, ~c":zl") == {:yes, [%{name: "zlib", data: :zlib, kind: :module}]} end test "Erlang module no completion", %{runtime: runtime} do @@ -123,17 +123,17 @@ defmodule NextLS.AutocompleteTest do test "Elixir proxy", %{runtime: runtime} do {:yes, [elixir_entry | _list]} = expand(runtime, ~c"E") - assert %{name: "Elixir", kind: :module, docs: "## Elixir" <> _} = elixir_entry + assert %{name: "Elixir", kind: :module} = elixir_entry end test "Elixir completion", %{runtime: runtime} do assert {:yes, [ - %{name: "Enum", kind: :module, docs: "## Enum" <> _}, - %{name: "Enumerable", kind: :module, docs: "## Enumerable" <> _} + %{name: "Enum", kind: :module}, + %{name: "Enumerable", kind: :module} ]} = expand(runtime, ~c"En") - assert {:yes, [%{name: "Enumerable", kind: :module, docs: "## Enumerable" <> _}]} = expand(runtime, ~c"Enumera") + assert {:yes, [%{name: "Enumerable", kind: :module}]} = expand(runtime, ~c"Enumera") end # test "Elixir type completion", %{runtime: runtime} do @@ -272,7 +272,7 @@ defmodule NextLS.AutocompleteTest do end test "Elixir root submodule completion", %{runtime: runtime} do - {:yes, [%{name: "Access", kind: :module, docs: "## Access" <> _}]} = assert expand(runtime, ~c"Elixir.Acce") + {:yes, [%{name: "Access", kind: :module}]} = assert expand(runtime, ~c"Elixir.Acce") end test "Elixir submodule completion", %{runtime: runtime} do @@ -284,56 +284,56 @@ defmodule NextLS.AutocompleteTest do end test "function completion", %{runtime: runtime} do - assert {:yes, [%{arity: 0, name: "version", docs: _, kind: :function}]} = expand(runtime, ~c"System.ve") + assert {:yes, [%{arity: 0, name: "version", kind: :function}]} = expand(runtime, ~c"System.ve") - assert {:yes, [%{arity: 1, name: "fun2ms", docs: _, kind: :function}]} = expand(runtime, ~c":ets.fun2") + assert {:yes, [%{arity: 1, name: "fun2ms", kind: :function}]} = expand(runtime, ~c":ets.fun2") end test "function completion with arity", %{runtime: runtime} do assert {:yes, [ - %{arity: 1, name: "printable?", docs: _, kind: :function}, - %{arity: 2, name: "printable?", docs: _, kind: :function} + %{arity: 1, name: "printable?", kind: :function}, + %{arity: 2, name: "printable?", kind: :function} ]} = expand(runtime, ~c"String.printable?") assert {:yes, [ - %{arity: 1, name: "printable?", docs: _, kind: :function}, - %{arity: 2, name: "printable?", docs: _, kind: :function} + %{arity: 1, name: "printable?", kind: :function}, + %{arity: 2, name: "printable?", kind: :function} ]} = expand(runtime, ~c"String.printable?/") assert {:yes, [ - %{arity: 1, name: "count", docs: _, kind: :function}, - %{arity: 2, name: "count", docs: _, kind: :function}, - %{arity: 2, name: "count_until", docs: _, kind: :function}, - %{arity: 3, name: "count_until", docs: _, kind: :function} + %{arity: 1, name: "count", kind: :function}, + %{arity: 2, name: "count", kind: :function}, + %{arity: 2, name: "count_until", kind: :function}, + %{arity: 3, name: "count_until", kind: :function} ]} = expand(runtime, ~c"Enum.count") assert {:yes, [ - %{arity: 1, name: "count", docs: _, kind: :function}, - %{arity: 2, name: "count", docs: _, kind: :function} + %{arity: 1, name: "count", kind: :function}, + %{arity: 2, name: "count", kind: :function} ]} = expand(runtime, ~c"Enum.count/") end test "operator completion", %{runtime: runtime} do assert {:yes, [ - %{arity: 1, name: "+", docs: _, kind: :function}, - %{arity: 2, name: "+", docs: _, kind: :function}, - %{arity: 2, name: "++", docs: _, kind: :function} + %{arity: 1, name: "+", kind: :function}, + %{arity: 2, name: "+", kind: :function}, + %{arity: 2, name: "++", kind: :function} ]} = expand(runtime, ~c"+") assert {:yes, [ - %{arity: 1, name: "+", docs: _, kind: :function}, - %{arity: 2, name: "+", docs: _, kind: :function} + %{arity: 1, name: "+", kind: :function}, + %{arity: 2, name: "+", kind: :function} ]} = expand(runtime, ~c"+/") assert {:yes, [ - %{arity: 2, name: "++", docs: _, kind: :function} + %{arity: 2, name: "++", kind: :function} ]} = expand(runtime, ~c"++/") end @@ -423,22 +423,22 @@ defmodule NextLS.AutocompleteTest do assert is_list(list) Enum.any?(list, fn i -> - match?(%{name: "unquote", arity: 1, kind: :function, docs: _}, i) + match?(%{name: "unquote", arity: 1, kind: :function}, i) end) Enum.any?(list, fn i -> - match?(%{name: "try", arity: 1, kind: :function, docs: _}, i) + match?(%{name: "try", arity: 1, kind: :function}, i) end) end # test "kernel import completion", %{runtime: runtime} do - # assert {:yes, [%{name: "defstruct", kind: :function, docs: _, arity: 1}]} = expand(runtime, ~c"defstru") + # assert {:yes, [%{name: "defstruct", kind: :function, arity: 1}]} = expand(runtime, ~c"defstru") # assert {:yes, # [ - # %{arity: 3, name: "put_elem", docs: _, kind: :function}, - # %{arity: 2, name: "put_in", docs: _, kind: :function}, - # %{arity: 3, name: "put_in", docs: _, kind: :function} + # %{arity: 3, name: "put_elem", kind: :function}, + # %{arity: 2, name: "put_in", kind: :function}, + # %{arity: 3, name: "put_in", kind: :function} # ]} = expand(runtime, ~c"put_") # end @@ -469,27 +469,9 @@ defmodule NextLS.AutocompleteTest do :yes, [ %{name: "nothing", kind: :variable}, - %{ - arity: 0, - name: "node", - docs: - "## Kernel.node/0\n\nReturns an atom representing the name of the local node.\nIf the node is not alive, `:nonode@nohost` is returned instead.\n\nAllowed in guard tests. Inlined by the compiler.\n\n", - kind: :function - }, - %{ - arity: 1, - name: "node", - docs: - "## Kernel.node/1\n\nReturns an atom representing the name of the local node.\nIf the node is not alive, `:nonode@nohost` is returned instead.\n\nAllowed in guard tests. Inlined by the compiler.\n\n", - kind: :function - }, - %{ - arity: 1, - name: "not", - docs: - "## Kernel.not/1\n\nStrictly boolean \"not\" operator.\n\n`value` must be a boolean; if it's not, an `ArgumentError` exception is raised.\n\nAllowed in guard tests. Inlined by the compiler.\n\n## Examples\n\n iex> not false\n true\n\n\n", - kind: :function - } + %{arity: 0, kind: :function, name: "node", data: {Kernel, "node", 0}}, + %{arity: 1, kind: :function, name: "node", data: {Kernel, "node", 1}}, + %{arity: 1, kind: :function, name: "not", data: {Kernel, "not", 1}} ] } end @@ -529,7 +511,7 @@ defmodule NextLS.AutocompleteTest do # end test "kernel special form completion", %{runtime: runtime} do - assert {:yes, [%{arity: 1, name: "unquote_splicing", docs: _, kind: :function}]} = expand(runtime, ~c"unquote_spl") + assert {:yes, [%{arity: 1, name: "unquote_splicing", kind: :function}]} = expand(runtime, ~c"unquote_spl") end test "completion inside expression", %{runtime: runtime} do @@ -544,7 +526,7 @@ defmodule NextLS.AutocompleteTest do test "Elixir completion sublevel", %{runtime: runtime} do assert expand(runtime, ~c"SublevelTest.") == - {:yes, [%{name: "LevelA", kind: :module, docs: "## SublevelTest.LevelA.LevelB\n\n\n"}]} + {:yes, [%{name: "LevelA", data: SublevelTest.LevelA.LevelB, kind: :module}]} end # TODO: aliases @@ -665,25 +647,24 @@ defmodule NextLS.AutocompleteTest do test "completion for bitstring modifiers", %{runtime: runtime} do assert {:yes, entries} = expand(runtime, ~c"< %{"completions" => %{"enable" => true}}} + defmacrop assert_match({:in, _, [left, right]}) do quote do assert Enum.any?(unquote(right), fn x -> @@ -306,29 +307,22 @@ defmodule NextLS.CompletionsTest do end """) - {results, log} = - with_log(fn -> - request client, %{ - method: "textDocument/completion", - id: 2, - jsonrpc: "2.0", - params: %{ - textDocument: %{ - uri: uri - }, - position: %{ - line: 2, - character: 11 - } - } + request client, %{ + method: "textDocument/completion", + id: 2, + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: uri + }, + position: %{ + line: 2, + character: 11 } + } + } - assert_result 2, [_, _, _] = results - results - end) - - assert log =~ "Could not locate cursor" - assert log =~ "Source code that produced the above warning:" + assert_result 2, [_, _, _] = results assert %{ "data" => nil, @@ -355,6 +349,44 @@ defmodule NextLS.CompletionsTest do } in results end + test "inside interpolation in strings", %{client: client, foo: foo} do + uri = uri(foo) + + did_open(client, foo, ~S""" + defmodule Foo do + def run(thing) do + "./lib/#{t}" + :ok + end + end + """) + + request client, %{ + method: "textDocument/completion", + id: 2, + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: uri + }, + position: %{ + line: 2, + character: 13 + } + } + } + + assert_result 2, results + + assert %{ + "data" => nil, + "documentation" => "", + "insertText" => "thing", + "kind" => 6, + "label" => "thing" + } in results + end + test "defmodule infer name", %{client: client, foo: foo} do uri = uri(foo) @@ -377,16 +409,14 @@ defmodule NextLS.CompletionsTest do } } - assert_result 2, [ - %{ - "data" => nil, - "documentation" => _, - "insertText" => "defmodule ${1:Foo} do\n $0\nend\n", - "kind" => 15, - "label" => "defmodule/2", - "insertTextFormat" => 2 - } - ] + assert_result 2, results + + assert_match %{ + "insertText" => "defmodule ${1:Foo} do\n $0\nend\n", + "kind" => 15, + "label" => "defmodule/2", + "insertTextFormat" => 2 + } in results end test "aliases in document", %{client: client, foo: foo} do @@ -419,9 +449,7 @@ defmodule NextLS.CompletionsTest do assert_result 2, results - assert_match( - %{"data" => _, "documentation" => _, "insertText" => "Bing", "kind" => 9, "label" => "Bing"} in results - ) + assert_match %{"data" => _, "insertText" => "Bing", "kind" => 9, "label" => "Bing"} in results end test "inside alias special form", %{client: client, foo: foo} do @@ -529,21 +557,21 @@ defmodule NextLS.CompletionsTest do assert_result 2, [ %{ - "data" => nil, + "data" => _, "documentation" => "", "insertText" => "var", "kind" => 6, "label" => "var" }, %{ - "data" => nil, + "data" => _, "documentation" => _, "insertText" => "var!", "kind" => 3, "label" => "var!/1" }, %{ - "data" => nil, + "data" => _, "documentation" => _, "insertText" => "var!", "kind" => 3, @@ -551,4 +579,160 @@ defmodule NextLS.CompletionsTest do } ] end + + test "variable and param completions", %{client: client, foo: foo} do + uri = uri(foo) + + did_open(client, foo, """ + defmodule Foo do + def run(%Bar{one: %{foo: %{bar: villain}}, two: vim}, vroom) do + document = vroom.assigns.documents[vim] + v + rescue + _ -> + :error + end + end + """) + + request client, %{ + method: "textDocument/completion", + id: 2, + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: uri + }, + position: %{ + line: 3, + character: 5 + } + } + } + + assert_result 2, results + + # assert_match %{"kind" => 6, "label" => "vampire"} in results + assert_match %{"kind" => 6, "label" => "villain"} in results + assert_match %{"kind" => 6, "label" => "vim"} in results + # assert_match %{"kind" => 6, "label" => "vrest"} in results + assert_match %{"kind" => 6, "label" => "vroom"} in results + # assert_match %{"kind" => 6, "label" => "var"} in results + end + + test "variable and param completions in other block identifiers", %{client: client, foo: foo} do + uri = uri(foo) + + did_open(client, foo, """ + defmodule Foo do + def run(%Bar{one: %{foo: %{bar: villain}}, two: vim}, vroom) do + var1 = vroom.assigns.documents[vim] + v + rescue + verror -> + var2 = "hi" + + v + end + end + """) + + request client, %{ + method: "textDocument/completion", + id: 2, + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: uri + }, + position: %{ + line: 8, + character: 7 + } + } + } + + assert_result 2, results + + assert_match %{"kind" => 6, "label" => "villain"} in results + assert_match %{"kind" => 6, "label" => "vim"} in results + assert_match %{"kind" => 6, "label" => "vroom"} in results + assert_match %{"kind" => 6, "label" => "verror"} in results + assert_match %{"kind" => 6, "label" => "var2"} in results + + assert_match %{"kind" => 6, "label" => "var1"} not in results + end + + test "param completions in multi arrow situations", %{client: client, foo: foo} do + uri = uri(foo) + + did_open(client, foo, """ + defmodule Foo do + def run(alice) do + alice + |> then(fn + {:ok, ast1} -> ast1 + {:error, ast2, _} -> a + {:error, :no_fuel_remaining} -> nil + end) + end + end + """) + + request client, %{ + method: "textDocument/completion", + id: 2, + jsonrpc: "2.0", + params: %{ + textDocument: %{uri: uri}, + position: %{ + line: 5, + character: 28 + } + } + } + + assert_result 2, results + + assert_match %{"kind" => 6, "label" => "alice"} in results + # TODO: requires changes to spitfire + # assert_match %{"kind" => 6, "label" => "ast2"} in results + + assert_match %{"kind" => 6, "label" => "ast1"} not in results + end + + test "variables show up in test blocks", %{client: client, foo: foo} do + uri = uri(foo) + + did_open(client, foo, """ + defmodule Foo do + use ExUnit.Case + test "something", %{vim: vim} do + var = "hi" + + v + end + end + """) + + request client, %{ + method: "textDocument/completion", + id: 2, + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: uri + }, + position: %{ + line: 5, + character: 5 + } + } + } + + assert_result 2, results + + assert_match %{"kind" => 6, "label" => "var"} in results + assert_match %{"kind" => 6, "label" => "vim"} in results + end end diff --git a/test/next_ls/helpers/hover_helpers_test.exs b/test/next_ls/docs_test.exs similarity index 95% rename from test/next_ls/helpers/hover_helpers_test.exs rename to test/next_ls/docs_test.exs index 012502f6..75ac8782 100644 --- a/test/next_ls/helpers/hover_helpers_test.exs +++ b/test/next_ls/docs_test.exs @@ -1,7 +1,7 @@ -defmodule NextLS.HoverHelpersTest do +defmodule NextLS.DocsTest do use ExUnit.Case, async: true - alias NextLS.HoverHelpers + alias NextLS.Docs describe "converts erlang html format to markdown" do test "some divs and p and code" do @@ -35,7 +35,7 @@ defmodule NextLS.HoverHelpersTest do ]} ] - actual = HoverHelpers.to_markdown("application/erlang+html", html) + actual = Docs.to_markdown("application/erlang+html", html) assert actual == String.trim(""" @@ -60,7 +60,7 @@ defmodule NextLS.HoverHelpersTest do ]} ] - actual = HoverHelpers.to_markdown("application/erlang+html", html) + actual = Docs.to_markdown("application/erlang+html", html) assert actual == String.trim(""" @@ -103,7 +103,7 @@ defmodule NextLS.HoverHelpersTest do {:p, [], ["Allowed in guard tests."]} ] - actual = HoverHelpers.to_markdown("application/erlang+html", html) + actual = Docs.to_markdown("application/erlang+html", html) assert actual == String.trim(""" @@ -191,7 +191,7 @@ defmodule NextLS.HoverHelpersTest do ]} ] - actual = HoverHelpers.to_markdown("application/erlang+html", html) + actual = Docs.to_markdown("application/erlang+html", html) assert String.trim(actual) == String.trim(""" @@ -231,7 +231,7 @@ defmodule NextLS.HoverHelpersTest do {:p, [], ["Returns ", {:code, [], ["error"]}, " if no value is associated with ", {:code, [], ["Flag"]}, "."]} ] - actual = HoverHelpers.to_markdown("application/erlang+html", html) + actual = Docs.to_markdown("application/erlang+html", html) assert String.trim(actual) == String.trim(""" diff --git a/test/next_ls/helpers/ast_helpers/env_test.exs b/test/next_ls/helpers/ast_helpers/env_test.exs deleted file mode 100644 index 1be03d13..00000000 --- a/test/next_ls/helpers/ast_helpers/env_test.exs +++ /dev/null @@ -1,217 +0,0 @@ -defmodule NextLS.ASTHelpers.EnvTest do - use ExUnit.Case, async: true - - describe "build/2" do - test "collects simple variables" do - code = """ - defmodule Foo do - def one do - foo = :bar - - Enum.map([foo], fn -> - bar = x - __cursor__() - end - - def two do - baz = :bar - end - end - """ - - actual = run(code) - - assert actual.variables == ["foo", "bar"] - end - - test "collects variables from patterns" do - code = """ - defmodule Foo do - def one() do - %{bar: [one, %{baz: two}]} = Some.thing() - - __cursor__() - end - - def two do - baz = :bar - end - end - """ - - actual = run(code) - - assert actual.variables == ["two", "one"] - end - - test "collects variables from 'formal' parameters" do - code = """ - defmodule Foo do - def zero(notme) do - :error - end - - def one(foo, bar, baz) do - - __cursor__() - end - - def two do - baz = :bar - end - end - """ - - actual = run(code) - - assert actual.variables == ["baz", "bar", "foo"] - end - - test "collects variables from stab parameters" do - code = """ - defmodule Foo do - def one() do - Enum.map(Some.thing(), fn - four -> - :ok - - one, two, three -> - __cursor__() - end - - def two do - baz = :bar - end - end - """ - - actual = run(code) - - assert actual.variables == ["three", "two", "one"] - end - - test "collects variables from left stab" do - code = """ - defmodule Foo do - def one() do - with [foo] <- thing(), - bar <- thang() do - __cursor__() - end - - def two do - baz = :bar - end - end - """ - - actual = run(code) - - assert actual.variables == ["foo", "bar"] - end - - test "scopes variables lexically" do - code = """ - defmodule Foo do - def one() do - baz = Some.thing() - foo = Enum.map(two(), fn bar -> - big_bar = bar * 2 - __cursor__() - end - - def two do - baz = :bar - end - end - """ - - actual = run(code) - - assert actual.variables == ["baz", "bar", "big_bar"] - end - - test "comprehension and with parameters do not leak" do - code = """ - defmodule Foo do - def one(entries) do - with {:ok, entry} <- entries do - :ok - end - - for entry <- entries do - :ok - end - - __cursor__() - end - - def two do - baz = :bar - end - end - """ - - actual = run(code) - - assert actual.variables == ["entries"] - end - - test "comprehension lhs of generator do not leak into rhs " do - code = """ - defmodule Foo do - def one(entries) do - for entry <- entries, - not_me <- __cursor__() do - :ok - end - end - - def two do - baz = :bar - end - end - """ - - actual = run(code) - - assert actual.variables == ["entries", "entry"] - end - - test "multiple generators and filters in comprehension" do - code = """ - defmodule Foo do - def one(entries) do - for entry <- entries, - foo = do_something(), - bar <- foo do - __cursor__() - :ok - end - end - - def two do - baz = :bar - end - end - """ - - actual = run(code) - - assert actual.variables == ["entries", "entry", "foo", "bar"] - end - end - - defp run(code) do - {:ok, zip} = - code - |> Spitfire.parse(literal_encoder: &{:ok, {:__literal__, &2, [&1]}}) - |> then(fn - {:ok, ast} -> ast - {:error, ast, _} -> ast - end) - |> NextLS.ASTHelpers.find_cursor() - - NextLS.ASTHelpers.Env.build(zip) - end -end diff --git a/test/next_ls/hover_test.exs b/test/next_ls/hover_test.exs index 61e27a32..aede5bce 100644 --- a/test/next_ls/hover_test.exs +++ b/test/next_ls/hover_test.exs @@ -195,7 +195,7 @@ defmodule NextLS.HoverTest do "contents" => %{ "kind" => "markdown", "value" => - "## Atom.to_string/1\n\n" <> + "## Atom.to_string/1\n" <> _ }, "range" => %{ @@ -223,7 +223,7 @@ defmodule NextLS.HoverTest do %{ "contents" => %{ "kind" => "markdown", - "value" => "## Bar.Baz.q/0\n\nBar.Baz.q function" + "value" => "## Bar.Baz.q/0\n\n`q()`\n\nBar.Baz.q function" }, "range" => %{ "start" => %{"character" => 13, "line" => 12}, @@ -246,7 +246,10 @@ defmodule NextLS.HoverTest do } } - assert_result 9, nil, 500 + assert_result 9, %{ + "contents" => %{"kind" => "markdown", "value" => "## Bar.Fiz"}, + "range" => %{"end" => %{"character" => 11, "line" => 13}, "start" => %{"character" => 9, "line" => 13}} + } end test "function without docs", %{client: client, example: example} do @@ -282,7 +285,7 @@ defmodule NextLS.HoverTest do %{ "contents" => %{ "kind" => "markdown", - "value" => "## Kernel.to_string/1\n\nConverts the argument to a string" <> _ + "value" => "## Kernel.to_string/1\n\n`to_string(term)`\n\nConverts the argument to a string" <> _ }, "range" => %{ "start" => %{"character" => 9, "line" => 15}, @@ -310,7 +313,7 @@ defmodule NextLS.HoverTest do "contents" => %{ "kind" => "markdown", "value" => - "## :timer.sleep/1\n\nSuspends the process" <> + "## :timer.sleep/1\n\n`sleep/1`\n\nSuspends the process" <> _ }, "range" => %{ @@ -365,7 +368,9 @@ defmodule NextLS.HoverTest do %{ "contents" => %{ "kind" => "markdown", - "value" => "## Kernel.def/2\n\nDefines a public function with the given name and body" <> _ + "value" => + "## Kernel.def/2\n\n`def(call, expr \\\\ nil)`\n\nDefines a public function with the given name and body" <> + _ }, "range" => %{ "start" => %{"character" => 2, "line" => 9},