Skip to content

Commit a990116

Browse files
committed
return ranges from definition and implementations providers
1 parent b305409 commit a990116

File tree

13 files changed

+672
-266
lines changed

13 files changed

+672
-266
lines changed

apps/language_server/lib/language_server/location.ex

Lines changed: 110 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@ defmodule ElixirLS.LanguageServer.Location do
1111

1212
alias ElixirSense.Core.Metadata
1313
alias ElixirSense.Core.Parser
14-
alias ElixirSense.Core.Source
15-
alias ElixirSense.Core.State.ModFunInfo
14+
alias ElixirSense.Core.State.{ModFunInfo, TypeInfo}
1615
alias ElixirSense.Core.Normalized.Code, as: CodeNormalized
1716
require ElixirSense.Core.Introspection, as: Introspection
17+
alias ElixirLS.LanguageServer.Location.Erl
1818

1919
@type t :: %__MODULE__{
2020
type: :module | :function | :variable | :typespec | :macro | :attribute,
2121
file: String.t() | nil,
2222
line: pos_integer,
23-
column: pos_integer
23+
column: pos_integer,
24+
end_line: pos_integer,
25+
end_column: pos_integer
2426
}
25-
defstruct [:type, :file, :line, :column]
27+
defstruct [:type, :file, :line, :column, :end_line, :end_column]
2628

2729
@spec find_mod_fun_source(module, atom, non_neg_integer | {:gte, non_neg_integer} | :any) ::
2830
t() | nil
@@ -111,86 +113,93 @@ defmodule ElixirLS.LanguageServer.Location do
111113
# module docs point to the begin of a file
112114
# we get better results by regex
113115
# the downside is we don't handle arity well
114-
find_fun_position_in_erl_file(file, fun)
116+
# TODO check if this is still the case on OTP 27+
117+
if fun != nil do
118+
range = Erl.find_fun_range(file, fun)
119+
120+
if range do
121+
{range, :function}
122+
end
123+
else
124+
range = Erl.find_module_range(file, mod)
125+
126+
if range do
127+
{range, :module}
128+
end
129+
end
115130
else
116131
%Metadata{mods_funs_to_positions: mods_funs_to_positions} =
117132
Parser.parse_file(file, false, false, nil)
118133

119134
case get_function_position_using_metadata(mod, fun, arity, mods_funs_to_positions) do
120-
%ModFunInfo{} = mi ->
121-
# assume function head or first clause is last in metadata
122-
{List.last(mi.positions), ModFunInfo.get_category(mi)}
123-
124135
nil ->
125136
# not found in metadata, fall back to docs
126137
get_function_position_using_docs(mod, fun, arity)
138+
139+
%ModFunInfo{} = info ->
140+
{info_to_range(info), ModFunInfo.get_category(info)}
127141
end
128142
end
129143

130144
case result do
145+
{{{line, column}, {end_line, end_column}}, category} ->
146+
%__MODULE__{
147+
type: category,
148+
file: file,
149+
line: line,
150+
column: column,
151+
end_line: end_line,
152+
end_column: end_column
153+
}
154+
155+
# TODO remove
131156
{{line, column}, category} ->
132-
%__MODULE__{type: category, file: file, line: line, column: column}
157+
%__MODULE__{
158+
type: category,
159+
file: file,
160+
line: line,
161+
column: column,
162+
end_line: line,
163+
end_column: column
164+
}
133165

134166
_ ->
135167
nil
136168
end
137169
end
138170

139-
defp find_fun_position_in_erl_file(file, nil) do
140-
case find_line_by_regex(file, ~r/^-module/u) do
141-
nil -> nil
142-
position -> {position, :module}
143-
end
144-
end
145-
146-
defp find_fun_position_in_erl_file(file, name) do
147-
escaped =
148-
name
149-
|> Atom.to_string()
150-
|> Regex.escape()
151-
152-
case find_line_by_regex(file, ~r/^#{escaped}\b\(/u) do
153-
nil -> nil
154-
position -> {position, :function}
155-
end
156-
end
157-
158-
defp find_type_position_in_erl_file(file, name) do
159-
escaped =
160-
name
161-
|> Atom.to_string()
162-
|> Regex.escape()
163-
164-
find_line_by_regex(file, ~r/^-(typep?|opaque)\s#{escaped}\b/u)
165-
end
166-
167-
defp find_line_by_regex(file, regex) do
168-
index =
169-
file
170-
|> File.read!()
171-
|> Source.split_lines()
172-
|> Enum.find_index(&String.match?(&1, regex))
173-
174-
case index do
175-
nil -> nil
176-
i -> {i + 1, 1}
177-
end
178-
end
179-
180171
defp find_type_position(_, nil, _), do: nil
181172

182173
defp find_type_position({mod, file}, name, arity) do
183174
result =
184175
if String.ends_with?(file, ".erl") do
185-
find_type_position_in_erl_file(file, name)
176+
Erl.find_type_range(file, name)
186177
else
187178
file_metadata = Parser.parse_file(file, false, false, nil)
188179
get_type_position(file_metadata, mod, name, arity)
189180
end
190181

191182
case result do
183+
{{line, column}, {end_line, end_column}} ->
184+
%__MODULE__{
185+
type: :typespec,
186+
file: file,
187+
line: line,
188+
column: column,
189+
end_line: end_line,
190+
end_column: end_column
191+
}
192+
193+
# TODO remove
192194
{line, column} ->
193-
%__MODULE__{type: :typespec, file: file, line: line, column: column}
195+
%__MODULE__{
196+
type: :typespec,
197+
file: file,
198+
line: line,
199+
column: column,
200+
end_line: line,
201+
end_column: column
202+
}
194203

195204
_ ->
196205
nil
@@ -242,72 +251,58 @@ defmodule ElixirLS.LanguageServer.Location do
242251
false
243252
end)
244253
|> Enum.map(fn
245-
{{category, _function, _arity}, line, _, _, _} when is_integer(line) ->
246-
{{line, 1}, category}
247-
248-
{{category, _function, _arity}, keyword, _, _, _} when is_list(keyword) ->
249-
{{Keyword.get(keyword, :location, 1), 1}, category}
254+
{{category, _function, _arity}, anno, _, _, _} ->
255+
{anno_to_range(anno), category}
256+
end)
257+
|> Enum.min_by(fn {{position, _end_position}, _category} -> position end, &<=/2, fn ->
258+
nil
250259
end)
251-
|> Enum.min_by(fn {{line, 1}, _category} -> line end, &<=/2, fn -> nil end)
252260
end
253261
end
254262

255-
def get_type_position(metadata, module, type, arity) do
263+
defp get_type_position(metadata, module, type, arity) do
256264
case get_type_position_using_metadata(module, type, arity, metadata.types) do
257265
nil ->
258266
get_type_position_using_docs(module, type, arity)
259267

260-
%ElixirSense.Core.State.TypeInfo{positions: positions} ->
261-
List.last(positions)
268+
%TypeInfo{} = info ->
269+
info_to_range(info)
262270
end
263271
end
264272

265-
def get_type_position_using_docs(module, type_name, arity) do
273+
defp get_type_position_using_docs(module, type_name, arity) do
266274
case CodeNormalized.fetch_docs(module) do
267275
{:error, _} ->
268276
nil
269277

270278
{_, _, _, _, _, _, docs} ->
271279
docs
272280
|> Enum.filter(fn
273-
{{:type, ^type_name, doc_arity}, _line, _, _, _meta} ->
281+
{{:type, ^type_name, doc_arity}, _, _, _, _meta} ->
274282
Introspection.matches_arity?(doc_arity, arity)
275283

276284
_ ->
277285
false
278286
end)
279287
|> Enum.map(fn
280-
{{_category, _function, _arity}, line, _, _, _} when is_integer(line) ->
281-
{line, 1}
282-
283-
{{_category, _function, _arity}, keyword, _, _, _} when is_list(keyword) ->
284-
{Keyword.get(keyword, :location, 1), 1}
288+
{{_category, _function, _arity}, anno, _, _, _} ->
289+
anno_to_range(anno)
285290
end)
286-
|> Enum.min_by(fn {line, 1} -> line end, &<=/2, fn -> nil end)
291+
|> Enum.min_by(fn {position, _end_position} -> position end, &<=/2, fn -> nil end)
287292
end
288293
end
289294

290-
def get_function_position_using_metadata(
291-
mod,
292-
fun,
293-
call_arity,
294-
mods_funs_to_positions,
295-
predicate \\ fn _ -> true end
296-
)
297-
295+
# TODO move to Metadata
298296
def get_function_position_using_metadata(
299297
mod,
300298
nil,
301299
_call_arity,
302-
mods_funs_to_positions,
303-
predicate
300+
mods_funs_to_positions
304301
) do
305302
mods_funs_to_positions
306303
|> Enum.find_value(fn
307304
{{^mod, nil, nil}, fun_info} ->
308-
if predicate.(fun_info) do
309-
fun_info
310-
end
305+
fun_info
311306

312307
_ ->
313308
false
@@ -318,30 +313,29 @@ defmodule ElixirLS.LanguageServer.Location do
318313
mod,
319314
fun,
320315
call_arity,
321-
mods_funs_to_positions,
322-
predicate
316+
mods_funs_to_positions
323317
) do
324318
mods_funs_to_positions
325319
|> Enum.filter(fn
326320
{{^mod, ^fun, fn_arity}, fun_info} when not is_nil(fn_arity) ->
327321
# assume function head is first in code and last in metadata
328322
default_args = fun_info.params |> Enum.at(-1) |> Introspection.count_defaults()
329323

330-
Introspection.matches_arity_with_defaults?(fn_arity, default_args, call_arity) and
331-
predicate.(fun_info)
324+
Introspection.matches_arity_with_defaults?(fn_arity, default_args, call_arity)
332325

333326
_ ->
334327
false
335328
end)
336329
|> min_by_line
337330
end
338331

339-
def get_type_position_using_metadata(mod, fun, call_arity, types, predicate \\ fn _ -> true end) do
332+
# TODO move to Metadata
333+
def get_type_position_using_metadata(mod, fun, call_arity, types) do
340334
types
341335
|> Enum.filter(fn
342-
{{^mod, ^fun, type_arity}, type_info}
336+
{{^mod, ^fun, type_arity}, _type_info}
343337
when not is_nil(type_arity) and Introspection.matches_arity?(type_arity, call_arity) ->
344-
predicate.(type_info)
338+
true
345339

346340
_ ->
347341
false
@@ -365,4 +359,30 @@ defmodule ElixirLS.LanguageServer.Location do
365359
nil -> nil
366360
end
367361
end
362+
363+
defp anno_to_range(anno) do
364+
line = :erl_anno.line(anno)
365+
366+
column =
367+
case :erl_anno.column(anno) do
368+
:undefined -> 1
369+
column -> column
370+
end
371+
372+
{end_line, end_column} =
373+
case :erl_anno.end_location(anno) do
374+
:undefined -> {line, column}
375+
{line, column} -> {line, column}
376+
line -> {line, 1}
377+
end
378+
379+
{{line, column}, {end_line, end_column}}
380+
end
381+
382+
def info_to_range(%{positions: positions, end_positions: end_positions}) do
383+
begin_position = List.last(positions)
384+
end_position = List.last(end_positions) || begin_position
385+
386+
{begin_position, end_position}
387+
end
368388
end

0 commit comments

Comments
 (0)