Skip to content

Commit a1aa976

Browse files
committed
Add call hierarchy provider
1 parent 53f66f4 commit a1aa976

File tree

8 files changed

+1642
-1
lines changed

8 files changed

+1642
-1
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
defmodule ElixirLS.LanguageServer.Providers.CallHierarchy do
2+
@moduledoc """
3+
This module provides textDocument/prepareCallHierarchy,
4+
callHierarchy/incomingCalls and callHierarchy/outgoingCalls support.
5+
6+
It enables finding all callers and callees of functions using the language server's
7+
tracer and metadata.
8+
9+
https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_prepareCallHierarchy
10+
"""
11+
12+
alias ElixirLS.LanguageServer.{SourceFile, Build, Parser}
13+
alias ElixirLS.LanguageServer.Providers.CallHierarchy.Locator
14+
require Logger
15+
16+
def prepare(
17+
%Parser.Context{source_file: source_file, metadata: metadata},
18+
uri,
19+
line,
20+
character,
21+
project_dir
22+
) do
23+
Build.with_build_lock(fn ->
24+
trace = ElixirLS.LanguageServer.Tracer.get_trace()
25+
26+
case Locator.prepare(source_file.text, line, character, trace, metadata: metadata) do
27+
nil ->
28+
nil
29+
30+
call_hierarchy_item ->
31+
# The LSP spec expects a list of CallHierarchyItem or null
32+
[convert_to_lsp_item(call_hierarchy_item, uri, source_file.text, project_dir)]
33+
end
34+
end)
35+
end
36+
37+
def incoming_calls(
38+
uri,
39+
name,
40+
kind,
41+
line,
42+
character,
43+
project_dir,
44+
source_file,
45+
parser_context
46+
) do
47+
Build.with_build_lock(fn ->
48+
trace = ElixirLS.LanguageServer.Tracer.get_trace()
49+
50+
Locator.incoming_calls(
51+
name,
52+
kind,
53+
{line, character},
54+
trace,
55+
metadata: parser_context.metadata,
56+
source_file: source_file
57+
)
58+
|> Enum.map(fn incoming_call ->
59+
convert_to_lsp_incoming_call(incoming_call, uri, project_dir)
60+
end)
61+
|> Enum.filter(&(not is_nil(&1)))
62+
|> Enum.uniq()
63+
end)
64+
end
65+
66+
def outgoing_calls(
67+
uri,
68+
name,
69+
kind,
70+
line,
71+
character,
72+
project_dir,
73+
source_file,
74+
parser_context
75+
) do
76+
Build.with_build_lock(fn ->
77+
trace = ElixirLS.LanguageServer.Tracer.get_trace()
78+
79+
Locator.outgoing_calls(
80+
name,
81+
kind,
82+
{line, character},
83+
trace,
84+
metadata: parser_context.metadata,
85+
source_file: source_file
86+
)
87+
|> Enum.map(fn outgoing_call ->
88+
convert_to_lsp_outgoing_call(outgoing_call, uri, project_dir)
89+
end)
90+
|> Enum.filter(&(not is_nil(&1)))
91+
|> Enum.uniq()
92+
end)
93+
end
94+
95+
defp convert_to_lsp_item(item, uri, text, project_dir) do
96+
{start_line, start_column} =
97+
SourceFile.elixir_position_to_lsp(text, {item.range.start.line, item.range.start.column})
98+
99+
{end_line, end_column} =
100+
SourceFile.elixir_position_to_lsp(text, {item.range.end.line, item.range.end.column})
101+
102+
{selection_start_line, selection_start_column} =
103+
SourceFile.elixir_position_to_lsp(
104+
text,
105+
{item.selection_range.start.line, item.selection_range.start.column}
106+
)
107+
108+
{selection_end_line, selection_end_column} =
109+
SourceFile.elixir_position_to_lsp(
110+
text,
111+
{item.selection_range.end.line, item.selection_range.end.column}
112+
)
113+
114+
uri = build_uri(item.uri, uri, project_dir)
115+
116+
%GenLSP.Structures.CallHierarchyItem{
117+
name: item.name,
118+
kind: item.kind,
119+
tags: item.tags,
120+
detail: item.detail,
121+
uri: uri,
122+
range: %GenLSP.Structures.Range{
123+
start: %GenLSP.Structures.Position{line: start_line, character: start_column},
124+
end: %GenLSP.Structures.Position{line: end_line, character: end_column}
125+
},
126+
selection_range: %GenLSP.Structures.Range{
127+
start: %GenLSP.Structures.Position{
128+
line: selection_start_line,
129+
character: selection_start_column
130+
},
131+
end: %GenLSP.Structures.Position{
132+
line: selection_end_line,
133+
character: selection_end_column
134+
}
135+
}
136+
}
137+
end
138+
139+
defp convert_to_lsp_incoming_call(incoming_call, current_uri, project_dir) do
140+
with {:ok, text} <- get_text(incoming_call.from.uri, current_uri),
141+
lsp_item <- convert_to_lsp_item(incoming_call.from, current_uri, text, project_dir) do
142+
ranges =
143+
incoming_call.from_ranges
144+
|> Enum.map(fn range ->
145+
{start_line, start_column} =
146+
SourceFile.elixir_position_to_lsp(text, {range.start.line, range.start.column})
147+
148+
{end_line, end_column} =
149+
SourceFile.elixir_position_to_lsp(text, {range.end.line, range.end.column})
150+
151+
%GenLSP.Structures.Range{
152+
start: %GenLSP.Structures.Position{line: start_line, character: start_column},
153+
end: %GenLSP.Structures.Position{line: end_line, character: end_column}
154+
}
155+
end)
156+
157+
%GenLSP.Structures.CallHierarchyIncomingCall{
158+
from: lsp_item,
159+
from_ranges: ranges
160+
}
161+
else
162+
_ -> nil
163+
end
164+
end
165+
166+
defp convert_to_lsp_outgoing_call(outgoing_call, current_uri, project_dir) do
167+
with {:ok, text} <- get_text(outgoing_call.to.uri, current_uri),
168+
lsp_item <- convert_to_lsp_item(outgoing_call.to, current_uri, text, project_dir) do
169+
ranges =
170+
outgoing_call.from_ranges
171+
|> Enum.map(fn range ->
172+
{start_line, start_column} =
173+
SourceFile.elixir_position_to_lsp(text, {range.start.line, range.start.column})
174+
175+
{end_line, end_column} =
176+
SourceFile.elixir_position_to_lsp(text, {range.end.line, range.end.column})
177+
178+
%GenLSP.Structures.Range{
179+
start: %GenLSP.Structures.Position{line: start_line, character: start_column},
180+
end: %GenLSP.Structures.Position{line: end_line, character: end_column}
181+
}
182+
end)
183+
184+
%GenLSP.Structures.CallHierarchyOutgoingCall{
185+
to: lsp_item,
186+
from_ranges: ranges
187+
}
188+
else
189+
_ -> nil
190+
end
191+
end
192+
193+
defp build_uri(nil, current_file_uri, _project_dir), do: current_file_uri
194+
195+
defp build_uri(path, _current_file_uri, project_dir) when is_binary(path) do
196+
SourceFile.Path.to_uri(path, project_dir)
197+
end
198+
199+
defp get_text(nil, current_text) when is_binary(current_text), do: {:ok, current_text}
200+
defp get_text("nofile", _), do: {:error, :nofile}
201+
defp get_text(path, _) when is_binary(path), do: File.read(path)
202+
end

0 commit comments

Comments
 (0)