From 9135e21810c03e4cfe17b37144f1991f3701a062 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 17 Jul 2023 08:25:03 +0200 Subject: [PATCH] provide docs for variables provide docs for variable when it shadows a function provide docs for all module attributes Partially addresses https://github.com/elixir-lsp/elixir_sense/issues/164 --- lib/elixir_sense.ex | 3 +- lib/elixir_sense/providers/docs.ex | 73 +++++++++++++++++++-- test/elixir_sense/docs_test.exs | 94 ++++++++++++++++++++++++++- test/elixir_sense/references_test.exs | 25 +++++++ 4 files changed, 186 insertions(+), 9 deletions(-) diff --git a/lib/elixir_sense.ex b/lib/elixir_sense.ex index 451e1dd3..f77ad6a8 100644 --- a/lib/elixir_sense.ex +++ b/lib/elixir_sense.ex @@ -67,11 +67,12 @@ defmodule ElixirSense do nil %{begin: begin_pos, end: end_pos, context: context} -> + {line, col} = begin_pos metadata = Parser.parse_string(code, true, true, line) env = Metadata.get_env(metadata, {line, column}) - case Docs.all(context, env, metadata.mods_funs_to_positions, metadata.types) do + case Docs.all(context, line, col, env, metadata.mods_funs_to_positions, metadata.types) do {actual_subject, docs} -> %{ actual_subject: actual_subject, diff --git a/lib/elixir_sense/providers/docs.ex b/lib/elixir_sense/providers/docs.ex index e639d4f4..fe3832b8 100644 --- a/lib/elixir_sense/providers/docs.ex +++ b/lib/elixir_sense/providers/docs.ex @@ -7,12 +7,22 @@ defmodule ElixirSense.Providers.Docs do alias ElixirSense.Core.Introspection alias ElixirSense.Core.ReservedWords alias ElixirSense.Core.State + alias ElixirSense.Core.State.VarInfo alias ElixirSense.Core.SurroundContext - @spec all(any, State.Env.t(), State.mods_funs_to_positions_t(), State.types_t()) :: + @spec all( + any, + pos_integer, + pos_integer, + State.Env.t(), + State.mods_funs_to_positions_t(), + State.types_t() + ) :: {actual_mod_fun :: String.t(), docs :: Introspection.docs()} | nil def all( context, + line, + column, %State.Env{ imports: imports, requires: requires, @@ -39,11 +49,62 @@ defmodule ElixirSense.Providers.Docs do {:keyword, keyword} -> docs = ReservedWords.docs(keyword) - {Atom.to_string(keyword), %{docs: docs}} + + {Atom.to_string(keyword), + %{ + docs: """ + > #{keyword} + + reserved word + + #{docs} + """ + }} {:attribute, attribute} -> - docs = BuiltinAttributes.docs(attribute) - if docs, do: {"@" <> Atom.to_string(attribute), %{docs: docs}} + docs = BuiltinAttributes.docs(attribute) || "" + + {"@" <> Atom.to_string(attribute), + %{ + docs: """ + > @#{attribute} + + module attribute + + #{docs} + """ + }} + + {:variable, variable} -> + var_info = + vars + |> Enum.find(fn + %VarInfo{name: name, positions: positions} -> + name == variable and {line, column} in positions + end) + + if var_info != nil do + {Atom.to_string(variable), + %{ + docs: """ + > #{variable} + + variable + """ + }} + else + mod_fun_docs( + type, + binding_env, + imports, + requires, + aliases, + module, + mods_funs, + metadata_types, + scope + ) + end _ -> mod_fun_docs( @@ -72,7 +133,6 @@ defmodule ElixirSense.Providers.Docs do scope ) do case Binding.expand(binding_env, type) do - # TODO :none -> mod_fun_docs( {nil, name}, @@ -85,6 +145,9 @@ defmodule ElixirSense.Providers.Docs do metadata_types, scope ) + + _ -> + nil end end diff --git a/test/elixir_sense/docs_test.exs b/test/elixir_sense/docs_test.exs index c8569e7a..30c0d35f 100644 --- a/test/elixir_sense/docs_test.exs +++ b/test/elixir_sense/docs_test.exs @@ -1028,8 +1028,10 @@ defmodule ElixirSense.DocsTest do assert %{ actual_subject: "@on_load", - docs: %{docs: "A hook that will be invoked whenever the module is loaded."} + docs: %{docs: docs} } = ElixirSense.docs(buffer, 2, 6) + + assert docs =~ "A hook that will be invoked whenever the module is loaded." end test "retrieve unreserved module attributes documentation" do @@ -1039,7 +1041,12 @@ defmodule ElixirSense.DocsTest do end """ - refute ElixirSense.docs(buffer, 2, 6) + assert %{ + actual_subject: "@my_attribute", + docs: %{docs: docs} + } = ElixirSense.docs(buffer, 2, 6) + + assert docs =~ "attribute" end test "retrieve docs on reserved words" do @@ -1050,7 +1057,88 @@ defmodule ElixirSense.DocsTest do assert %{ actual_subject: "do", - docs: %{docs: "do-end block control keyword"} + docs: %{docs: docs} } = ElixirSense.docs(buffer, 1, 21) + + assert docs =~ "do-end block control keyword" + end + + test "retrieve docs on variables" do + buffer = """ + defmodule MyModule do + def fun(my_var) do + other_var = 5 + abc(my_var, other_var) + end + end + """ + + assert %{ + actual_subject: "my_var", + docs: %{docs: docs} + } = ElixirSense.docs(buffer, 2, 12) + + assert docs =~ "variable" + + assert %{ + actual_subject: "other_var", + docs: %{docs: docs} + } = ElixirSense.docs(buffer, 3, 6) + + assert docs =~ "variable" + end + + test "variables shadow builtin functions" do + buffer = """ + defmodule Vector do + @spec magnitude(Vec2.t()) :: number() + def magnitude(%Vec2{} = v), do: :math.sqrt(:math.pow(v.x, 2) + :math.pow(v.y, 2)) + + @spec normalize(Vec2.t()) :: Vec2.t() + def normalize(%Vec2{} = v) do + length = magnitude(v) + %{v | x: v.x / length, y: v.y / length} + end + end + """ + + assert %{ + actual_subject: "length", + docs: %{docs: docs} + } = ElixirSense.docs(buffer, 7, 6) + + assert docs =~ "variable" + + assert %{ + actual_subject: "length", + docs: %{docs: docs} + } = ElixirSense.docs(buffer, 8, 21) + + assert docs =~ "variable" + end + + test "find local type in typespec local def elsewhere" do + buffer = """ + defmodule ElixirSenseExample.Some do + @type some_local :: integer + + def some_local(), do: :ok + + @type user :: {some_local, integer} + + def foo do + some_local + end + end + """ + + assert %{actual_subject: "ElixirSenseExample.Some.some_local"} = + ElixirSense.docs(buffer, 6, 20) + + # TODO assert type + assert %{actual_subject: "ElixirSenseExample.Some.some_local"} = + ElixirSense.docs(buffer, 9, 9) + + # TODO assert function end end diff --git a/test/elixir_sense/references_test.exs b/test/elixir_sense/references_test.exs index 1841b6db..ff207366 100644 --- a/test/elixir_sense/references_test.exs +++ b/test/elixir_sense/references_test.exs @@ -930,6 +930,31 @@ defmodule ElixirSense.Providers.ReferencesTest do assert ElixirSense.references(buffer, 8, 10, trace) == expected_references end + test "find references of a variable shadowing function", %{trace: trace} do + buffer = """ + defmodule Vector do + @spec magnitude(Vec2.t()) :: number() + def magnitude(%Vec2{} = v), do: :math.sqrt(:math.pow(v.x, 2) + :math.pow(v.y, 2)) + + @spec normalize(Vec2.t()) :: Vec2.t() + def normalize(%Vec2{} = v) do + length = magnitude(v) + %{v | x: v.x / length, y: v.y / length} + end + end + """ + + # `my_var` + expected_references = [ + %{uri: nil, range: %{start: %{line: 7, column: 5}, end: %{line: 7, column: 11}}}, + %{uri: nil, range: %{start: %{line: 8, column: 20}, end: %{line: 8, column: 26}}}, + %{uri: nil, range: %{start: %{line: 8, column: 37}, end: %{line: 8, column: 43}}} + ] + + assert ElixirSense.references(buffer, 7, 6, trace) == expected_references + assert ElixirSense.references(buffer, 8, 21, trace) == expected_references + end + test "find references of attributes", %{trace: trace} do buffer = """ defmodule MyModule do