Skip to content

Commit aee7242

Browse files
authored
Bump elixir_sense and use ElixirSense.references/3 (elixir-editors#82)
* Bump elixir_sense and use ElixirSense.references/3 Now that elixir-lsp/elixir_sense#42 is fixed (via elixir-lsp/elixir_sense#61) and umbrella support is added via elixir-lsp/elixir_sense#46 we no longer need to maintain a separate implementation of `ElixirSense.references/3` and can instead use the implementation provided by `ElixirSense`. This is good because now we're using less of the private internals of `ElixirSense`. * Map nil uri from ElixirSense to the current file * Upgrade elixir_sense after infinite loop * Handle types being an atom
1 parent 5886a5c commit aee7242

File tree

11 files changed

+239
-70
lines changed

11 files changed

+239
-70
lines changed

apps/elixir_ls_utils/test/support/test_utils.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ defmodule ElixirLS.Utils.TestUtils do
1010

1111
assert char == "^"
1212
end
13+
14+
def assert_match_list(list1, list2) do
15+
assert Enum.sort(list1) == Enum.sort(list2)
16+
end
1317
end

apps/language_server/lib/language_server/providers/completion.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,9 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
529529
origin: origin
530530
} = info
531531

532+
# ElixirSense now returns types as an atom
533+
type = to_string(type)
534+
532535
%{
533536
pipe_before?: pipe_before?,
534537
capture_before?: capture_before?,

apps/language_server/lib/language_server/providers/references.ex

Lines changed: 43 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,70 @@
11
defmodule ElixirLS.LanguageServer.Providers.References do
22
@moduledoc """
3-
This module provides References support by using
4-
the `Mix.Tasks.Xref.call/0` task to find all references to
5-
any function or module identified at the provided location.
3+
This module provides References support by using `ElixirSense.references/3` to
4+
find all references to any function or module identified at the provided
5+
location.
6+
7+
Does not support configuring "includeDeclaration" and assumes it is always
8+
`true`
9+
10+
https://microsoft.github.io//language-server-protocol/specifications/specification-3-14/#textDocument_references
611
"""
712
require Logger
813

914
alias ElixirLS.LanguageServer.{SourceFile, Build}
1015

11-
def references(text, line, character, _include_declaration) do
16+
def references(text, uri, line, character, _include_declaration) do
1217
Build.with_build_lock(fn ->
13-
xref_at_cursor(text, line, character)
14-
|> Enum.filter(fn %{line: line} -> is_integer(line) end)
15-
|> Enum.map(&build_location/1)
18+
ElixirSense.references(text, line + 1, character + 1)
19+
|> Enum.map(fn elixir_sense_reference ->
20+
elixir_sense_reference
21+
|> build_reference(uri)
22+
|> build_loc()
23+
end)
24+
|> Enum.filter(&has_uri?/1)
1625
end)
1726
end
1827

1928
def supported? do
2029
Mix.Tasks.Xref.__info__(:functions) |> Enum.member?({:calls, 0})
2130
end
2231

23-
defp xref_at_cursor(text, line, character) do
24-
env_at_cursor = line_environment(text, line)
25-
%{aliases: aliases} = env_at_cursor
26-
27-
subject_at_cursor(text, line, character)
28-
# TODO: Don't call into here directly
29-
|> ElixirSense.Core.Source.split_module_and_func(aliases)
30-
|> expand_mod_fun(env_at_cursor)
31-
|> add_arity(env_at_cursor)
32-
|> callers()
33-
end
34-
35-
defp line_environment(text, line) do
36-
# TODO: Don't call into here directly
37-
ElixirSense.Core.Parser.parse_string(text, true, true, line + 1)
38-
|> ElixirSense.Core.Metadata.get_env(line + 1)
39-
end
40-
41-
defp subject_at_cursor(text, line, character) do
42-
# TODO: Don't call into here directly
43-
ElixirSense.Core.Source.subject(text, line + 1, character + 1)
44-
end
45-
46-
defp expand_mod_fun({nil, nil}, _environment), do: nil
47-
48-
defp expand_mod_fun(mod_fun, %{imports: imports, aliases: aliases, module: module}) do
49-
# TODO: Don't call into here directly
50-
mod_fun = ElixirSense.Core.Introspection.actual_mod_fun(mod_fun, imports, aliases, module)
51-
52-
case mod_fun do
53-
{mod, nil} -> {mod, nil}
54-
{mod, fun} -> {mod, fun}
55-
end
32+
defp build_reference(ref, current_file_uri) do
33+
%{
34+
range: %{
35+
start: %{line: ref.range.start.line, column: ref.range.start.column},
36+
end: %{line: ref.range.end.line, column: ref.range.end.column}
37+
},
38+
uri: build_uri(ref, current_file_uri)
39+
}
5640
end
5741

58-
defp add_arity({mod, fun}, %{scope: {fun, arity}, module: mod}), do: {mod, fun, arity}
59-
defp add_arity({mod, fun}, _env), do: {mod, fun, nil}
60-
61-
defp callers(mfa) do
62-
if Mix.Project.umbrella?() do
63-
umbrella_calls()
64-
else
65-
Mix.Tasks.Xref.calls()
42+
def build_uri(elixir_sense_ref, current_file_uri) do
43+
case elixir_sense_ref.uri do
44+
# A `nil` uri indicates that the reference was in the passed in text
45+
# https://github.com/elixir-lsp/elixir-ls/pull/82#discussion_r351922803
46+
nil -> current_file_uri
47+
# ElixirSense returns a plain path (e.g. "/home/bob/my_app/lib/a.ex") as
48+
# the "uri" so we convert it to an actual uri
49+
path when is_binary(path) -> SourceFile.path_to_uri(path)
50+
_ -> nil
6651
end
67-
|> Enum.filter(caller_filter(mfa))
6852
end
6953

70-
def umbrella_calls() do
71-
build_dir = Path.expand(Mix.Project.config()[:build_path])
72-
73-
Mix.Project.apps_paths()
74-
|> Enum.flat_map(fn {app, path} ->
75-
Mix.Project.in_project(app, path, [build_path: build_dir], fn _ ->
76-
Mix.Tasks.Xref.calls()
77-
|> Enum.map(fn %{file: file} = call ->
78-
Map.put(call, :file, Path.expand(file))
79-
end)
80-
end)
81-
end)
82-
end
54+
defp has_uri?(reference), do: !is_nil(reference["uri"])
8355

84-
defp caller_filter({module, nil, nil}), do: &match?(%{callee: {^module, _, _}}, &1)
85-
defp caller_filter({module, func, nil}), do: &match?(%{callee: {^module, ^func, _}}, &1)
86-
defp caller_filter({module, func, arity}), do: &match?(%{callee: {^module, ^func, ^arity}}, &1)
56+
defp build_loc(reference) do
57+
# Adjust for ElixirSense 1-based indexing
58+
line_start = reference.range.start.line - 1
59+
line_end = reference.range.end.line - 1
60+
column_start = reference.range.start.column - 1
61+
column_end = reference.range.end.column - 1
8762

88-
defp build_location(%{file: file, line: line}) do
8963
%{
90-
"uri" => SourceFile.path_to_uri(file),
64+
"uri" => reference.uri,
9165
"range" => %{
92-
"start" => %{"line" => line - 1, "character" => 0},
93-
"end" => %{"line" => line - 1, "character" => 0}
66+
"start" => %{"line" => line_start, "character" => column_start},
67+
"end" => %{"line" => line_end, "character" => column_end}
9468
}
9569
}
9670
end

apps/language_server/lib/language_server/server.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ defmodule ElixirLS.LanguageServer.Server do
354354
{:ok,
355355
References.references(
356356
state.source_files[uri].text,
357+
uri,
357358
line,
358359
character,
359360
include_declaration
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do
2+
use ExUnit.Case, async: true
3+
4+
alias ElixirLS.LanguageServer.Providers.References
5+
require ElixirLS.Test.TextLoc
6+
7+
test "finds references to a function" do
8+
file_path = Path.join(__DIR__, "../../support/references_b.ex") |> Path.expand()
9+
text = File.read!(file_path)
10+
uri = "file://#{file_path}"
11+
12+
{line, char} = {2, 8}
13+
14+
ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """
15+
some_var = 42
16+
^
17+
""")
18+
19+
ElixirLS.Utils.TestUtils.assert_match_list(
20+
References.references(text, uri, line, char, true),
21+
[
22+
%{
23+
"range" => %{
24+
"start" => %{"line" => 3, "character" => 4},
25+
"end" => %{"line" => 3, "character" => 12}
26+
},
27+
"uri" => uri
28+
},
29+
%{
30+
"range" => %{
31+
"start" => %{"line" => 5, "character" => 12},
32+
"end" => %{"line" => 5, "character" => 20}
33+
},
34+
"uri" => uri
35+
}
36+
]
37+
)
38+
end
39+
40+
test "cannot find a references to a macro generated function call" do
41+
file_path = Path.join(__DIR__, "../../support/uses_macro_a.ex") |> Path.expand()
42+
text = File.read!(file_path)
43+
uri = "file://#{file_path}"
44+
{line, char} = {6, 13}
45+
46+
ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """
47+
macro_a_func()
48+
^
49+
""")
50+
51+
assert References.references(text, uri, line, char, true) == []
52+
end
53+
54+
test "finds a references to a macro imported function call" do
55+
file_path = Path.join(__DIR__, "../../support/uses_macro_a.ex") |> Path.expand()
56+
text = File.read!(file_path)
57+
uri = "file://#{file_path}"
58+
{line, char} = {10, 4}
59+
60+
ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """
61+
macro_imported_fun()
62+
^
63+
""")
64+
65+
assert References.references(text, uri, line, char, true) == [
66+
%{
67+
"range" => %{
68+
"start" => %{"line" => 10, "character" => 4},
69+
"end" => %{"line" => 10, "character" => 22}
70+
},
71+
"uri" => uri
72+
}
73+
]
74+
end
75+
76+
test "finds references to a variable" do
77+
file_path = Path.join(__DIR__, "../../support/references_b.ex") |> Path.expand()
78+
text = File.read!(file_path)
79+
uri = "file://#{file_path}"
80+
{line, char} = {4, 14}
81+
82+
ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """
83+
IO.puts(some_var + 1)
84+
^
85+
""")
86+
87+
assert References.references(text, uri, line, char, true) == [
88+
%{
89+
"range" => %{
90+
"end" => %{"character" => 12, "line" => 2},
91+
"start" => %{"character" => 4, "line" => 2}
92+
},
93+
"uri" => uri
94+
},
95+
%{
96+
"range" => %{
97+
"end" => %{"character" => 20, "line" => 4},
98+
"start" => %{"character" => 12, "line" => 4}
99+
},
100+
"uri" => uri
101+
}
102+
]
103+
end
104+
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
defmodule ElixirLS.Test.MacroA do
2+
defmacro __using__(_) do
3+
quote do
4+
import ElixirLS.Test.MacroA
5+
6+
def macro_a_func do
7+
:ok
8+
end
9+
end
10+
end
11+
12+
def macro_imported_fun do
13+
:ok
14+
end
15+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
defmodule ElixirLS.Test.ReferencesA do
2+
def a_fun do
3+
ElixirLS.Test.ReferencesB.b_fun()
4+
end
5+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
defmodule ElixirLS.Test.ReferencesB do
2+
def b_fun do
3+
some_var = 42
4+
5+
IO.puts(some_var + 1)
6+
:ok
7+
end
8+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
defmodule ElixirLS.Test.TextLoc do
2+
def annotate(path, line, character) do
3+
with {:ok, text} <- read_file_line(path, line) do
4+
pointer_line = String.duplicate(" ", character) <> "^\n"
5+
{:ok, text <> pointer_line}
6+
end
7+
end
8+
9+
defmacro annotate_assert(path, line, character, expected) do
10+
quote do
11+
assert {:ok, actual} =
12+
ElixirLS.Test.TextLoc.annotate(unquote(path), unquote(line), unquote(character))
13+
14+
if actual == unquote(expected) do
15+
assert actual == unquote(expected)
16+
else
17+
IO.puts("Acutal is:")
18+
IO.puts(["\"\"\"", "\n", actual, "\"\"\""])
19+
assert actual == unquote(expected)
20+
end
21+
end
22+
end
23+
24+
def read_file_line(path, line) do
25+
File.stream!(path, [:read, :utf8], :line)
26+
|> Stream.drop(line)
27+
|> Enum.take(1)
28+
|> hd()
29+
|> wrap_in_ok()
30+
rescue
31+
e in File.Error ->
32+
{:error, e}
33+
end
34+
35+
defp wrap_in_ok(input), do: {:ok, input}
36+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
defmodule ElixirLS.Test.UsesMacroA do
2+
use ElixirLS.Test.MacroA
3+
4+
@inputs [1, 2, 3]
5+
6+
def my_fun do
7+
macro_a_func()
8+
end
9+
10+
def my_other_fun do
11+
macro_imported_fun()
12+
end
13+
14+
for input <- @inputs do
15+
def gen_fun(unquote(input)) do
16+
unquote(input) + 1
17+
end
18+
end
19+
end

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
%{
22
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.7", "6287f8f2cb45df8584317a4be1075b8c9b8a69de8eeb82b4d9e6c761cf2664cd", [:mix], [{:erlex, ">= 0.2.5", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"},
3-
"elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "899db30568fa8decfe20b42a64fcb7f87798c3ef", []},
3+
"elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "877fa6f392546bcc037536eb316a13b0388ecb7b", []},
44
"erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []},
55
"erlex": {:hex, :erlex, "0.2.5", "e51132f2f472e13d606d808f0574508eeea2030d487fc002b46ad97e738b0510", [:mix], [], "hexpm"},
66
"forms": {:hex, :forms, "0.0.1", "45f3b10b6f859f95f2c2c1a1de244d63855296d55ed8e93eb0dd116b3e86c4a6", [:rebar3], [], "hexpm"},

0 commit comments

Comments
 (0)