Skip to content

Commit 2027cc8

Browse files
committed
Introduce browse code actions
* Browse elvis warnings * Browse compiler errors * Browse functions and types in otp docs or hex docs
1 parent ed1daaa commit 2027cc8

9 files changed

+610
-36
lines changed

apps/els_core/include/els_core.hrl

+2
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,8 @@
581581
%%------------------------------------------------------------------------------
582582

583583
-define(CODE_ACTION_KIND_QUICKFIX, <<"quickfix">>).
584+
-define(CODE_ACTION_KIND_BROWSE, <<"browse">>).
585+
584586
-type code_action_kind() :: binary().
585587

586588
-type code_action_context() :: #{

apps/els_core/src/els_config.erl

+11-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
initialize/4,
88
get/1,
99
set/2,
10-
start_link/0
10+
start_link/0,
11+
is_dep/1
1112
]).
1213

1314
%% gen_server callbacks
@@ -584,6 +585,15 @@ expand_var(Bin, [{Var, Value} | RestEnv]) ->
584585
[Value, RestBin]
585586
end.
586587

588+
-spec is_dep(string()) -> boolean().
589+
is_dep(Path) ->
590+
lists:any(
591+
fun(DepPath) ->
592+
lists:prefix(DepPath, Path)
593+
end,
594+
els_config:get(deps_paths)
595+
).
596+
587597
-spec get_env() -> [{string(), string()}].
588598
-if(?OTP_RELEASE >= 24).
589599
get_env() ->

apps/els_core/src/els_uri.erl

+17-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
-export([
1212
module/1,
1313
path/1,
14-
uri/1
14+
uri/1,
15+
app/1
1516
]).
1617

1718
%%==============================================================================
@@ -26,6 +27,21 @@
2627
%%==============================================================================
2728
-include("els_core.hrl").
2829

30+
-spec app(uri() | [binary()]) -> {ok, atom()} | error.
31+
app(Uri) when is_binary(Uri) ->
32+
app(lists:reverse(filename:split(path(Uri))));
33+
app([]) ->
34+
error;
35+
app([_File, <<"src">>, AppBin0 | _]) ->
36+
case binary:split(AppBin0, <<"-">>) of
37+
[AppBin, _Vsn] ->
38+
{ok, binary_to_atom(AppBin)};
39+
[AppBin] ->
40+
{ok, binary_to_atom(AppBin)}
41+
end;
42+
app([_ | Rest]) ->
43+
app(Rest).
44+
2945
-spec module(uri()) -> atom().
3046
module(Uri) ->
3147
binary_to_atom(filename:basename(path(Uri), <<".erl">>), utf8).

apps/els_lsp/src/els_code_action_provider.erl

+34-30
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ code_actions(Uri, Range, #{<<"diagnostics">> := Diagnostics}) ->
3434
lists:flatten([make_code_actions(Uri, D) || D <- Diagnostics]) ++
3535
wrangler_handler:get_code_actions(Uri, Range) ++
3636
els_code_actions:extract_function(Uri, Range) ++
37-
els_code_actions:bump_variables(Uri, Range)
37+
els_code_actions:bump_variables(Uri, Range) ++
38+
els_code_actions:browse_docs(Uri, Range)
3839
).
3940

4041
-spec make_code_actions(uri(), map()) -> [map()].
@@ -43,35 +44,38 @@ make_code_actions(
4344
#{<<"message">> := Message, <<"range">> := Range} = Diagnostic
4445
) ->
4546
Data = maps:get(<<"data">>, Diagnostic, <<>>),
46-
make_code_actions(
47-
[
48-
{"function (.*) is unused", fun els_code_actions:export_function/4},
49-
{"variable '(.*)' is unused", fun els_code_actions:ignore_variable/4},
50-
{"variable '(.*)' is unbound", fun els_code_actions:suggest_variable/4},
51-
{"undefined macro '(.*)'", fun els_code_actions:add_include_lib_macro/4},
52-
{"undefined macro '(.*)'", fun els_code_actions:define_macro/4},
53-
{"undefined macro '(.*)'", fun els_code_actions:suggest_macro/4},
54-
{"record (.*) undefined", fun els_code_actions:add_include_lib_record/4},
55-
{"record (.*) undefined", fun els_code_actions:define_record/4},
56-
{"record (.*) undefined", fun els_code_actions:suggest_record/4},
57-
{"field (.*) undefined in record (.*)", fun els_code_actions:suggest_record_field/4},
58-
{"Module name '(.*)' does not match file name '(.*)'",
59-
fun els_code_actions:fix_module_name/4},
60-
{"Unused macro: (.*)", fun els_code_actions:remove_macro/4},
61-
{"function (.*) undefined", fun els_code_actions:create_function/4},
62-
{"function (.*) undefined", fun els_code_actions:suggest_function/4},
63-
{"Cannot find definition for function (.*)", fun els_code_actions:suggest_function/4},
64-
{"Cannot find module (.*)", fun els_code_actions:suggest_module/4},
65-
{"Unused file: (.*)", fun els_code_actions:remove_unused/4},
66-
{"Atom typo\\? Did you mean: (.*)", fun els_code_actions:fix_atom_typo/4},
67-
{"undefined callback function (.*) \\\(behaviour '(.*)'\\\)",
68-
fun els_code_actions:undefined_callback/4}
69-
],
70-
Uri,
71-
Range,
72-
Data,
73-
Message
74-
).
47+
els_code_actions:browse_error(Diagnostic) ++
48+
make_code_actions(
49+
[
50+
{"function (.*) is unused", fun els_code_actions:export_function/4},
51+
{"variable '(.*)' is unused", fun els_code_actions:ignore_variable/4},
52+
{"variable '(.*)' is unbound", fun els_code_actions:suggest_variable/4},
53+
{"undefined macro '(.*)'", fun els_code_actions:add_include_lib_macro/4},
54+
{"undefined macro '(.*)'", fun els_code_actions:define_macro/4},
55+
{"undefined macro '(.*)'", fun els_code_actions:suggest_macro/4},
56+
{"record (.*) undefined", fun els_code_actions:add_include_lib_record/4},
57+
{"record (.*) undefined", fun els_code_actions:define_record/4},
58+
{"record (.*) undefined", fun els_code_actions:suggest_record/4},
59+
{"field (.*) undefined in record (.*)",
60+
fun els_code_actions:suggest_record_field/4},
61+
{"Module name '(.*)' does not match file name '(.*)'",
62+
fun els_code_actions:fix_module_name/4},
63+
{"Unused macro: (.*)", fun els_code_actions:remove_macro/4},
64+
{"function (.*) undefined", fun els_code_actions:create_function/4},
65+
{"function (.*) undefined", fun els_code_actions:suggest_function/4},
66+
{"Cannot find definition for function (.*)",
67+
fun els_code_actions:suggest_function/4},
68+
{"Cannot find module (.*)", fun els_code_actions:suggest_module/4},
69+
{"Unused file: (.*)", fun els_code_actions:remove_unused/4},
70+
{"Atom typo\\? Did you mean: (.*)", fun els_code_actions:fix_atom_typo/4},
71+
{"undefined callback function (.*) \\\(behaviour '(.*)'\\\)",
72+
fun els_code_actions:undefined_callback/4}
73+
],
74+
Uri,
75+
Range,
76+
Data,
77+
Message
78+
).
7579

7680
-spec make_code_actions([{string(), Fun}], uri(), range(), binary(), binary()) ->
7781
[map()]

apps/els_lsp/src/els_code_actions.erl

+122-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
suggest_record_field/4,
2020
suggest_function/4,
2121
suggest_module/4,
22-
bump_variables/2
22+
bump_variables/2,
23+
browse_error/1,
24+
browse_docs/2
2325
]).
2426

2527
-include("els_lsp.hrl").
@@ -577,6 +579,125 @@ undefined_callback(Uri, _Range, _Data, [_Function, Behaviour]) ->
577579
}
578580
].
579581

582+
-spec browse_docs(uri(), range()) -> [map()].
583+
browse_docs(Uri, Range) ->
584+
#{from := {Line, Column}} = els_range:to_poi_range(Range),
585+
{ok, Document} = els_utils:lookup_document(Uri),
586+
POIs = els_dt_document:get_element_at_pos(Document, Line, Column),
587+
lists:flatten([browse_docs(POI) || POI <- POIs]).
588+
589+
-spec browse_docs(els_poi:poi()) -> [map()].
590+
browse_docs(#{id := {M, F, A}, kind := Kind}) when
591+
Kind == application;
592+
Kind == type_application
593+
->
594+
case els_utils:find_module(M) of
595+
{ok, ModUri} ->
596+
case els_uri:app(ModUri) of
597+
{ok, App} ->
598+
DocType = doc_type(ModUri),
599+
make_browse_docs_command(DocType, {M, F, A}, App, Kind);
600+
error ->
601+
[]
602+
end;
603+
{error, not_found} ->
604+
[]
605+
end;
606+
browse_docs(_) ->
607+
[].
608+
609+
-spec doc_type(uri()) -> otp | hex | other.
610+
doc_type(Uri) ->
611+
Path = binary_to_list(els_uri:path(Uri)),
612+
OtpPath = els_config:get(otp_path),
613+
case lists:prefix(OtpPath, Path) of
614+
true ->
615+
otp;
616+
false ->
617+
case els_config:is_dep(Path) of
618+
true ->
619+
hex;
620+
false ->
621+
other
622+
end
623+
end.
624+
625+
-spec make_browse_docs_command(atom(), mfa(), atom(), atom()) ->
626+
[map()].
627+
make_browse_docs_command(other, _MFA, _App, _Kind) ->
628+
[];
629+
make_browse_docs_command(DocType, {M, F, A}, App, Kind) ->
630+
Title = make_browse_docs_title(DocType, {M, F, A}),
631+
[
632+
#{
633+
title => Title,
634+
kind => ?CODE_ACTION_KIND_BROWSE,
635+
command =>
636+
els_command:make_command(
637+
Title,
638+
<<"browse-docs">>,
639+
[
640+
#{
641+
source => DocType,
642+
module => M,
643+
function => F,
644+
arity => A,
645+
app => App,
646+
kind => els_dt_references:kind_to_category(Kind)
647+
}
648+
]
649+
)
650+
}
651+
].
652+
653+
-spec make_browse_docs_title(atom(), mfa()) -> binary().
654+
make_browse_docs_title(otp, {M, F, A}) ->
655+
list_to_binary(io_lib:format("Browse: OTP docs: ~p:~p/~p", [M, F, A]));
656+
make_browse_docs_title(hex, {M, F, A}) ->
657+
list_to_binary(io_lib:format("Browse: Hex docs: ~p:~p/~p", [M, F, A])).
658+
659+
-spec browse_error(map()) -> [map()].
660+
browse_error(#{<<"source">> := <<"Compiler">>, <<"code">> := ErrorCode}) ->
661+
Title = <<"Browse: Erlang Error Index: ", ErrorCode/binary>>,
662+
[
663+
#{
664+
title => Title,
665+
kind => ?CODE_ACTION_KIND_BROWSE,
666+
command =>
667+
els_command:make_command(
668+
Title,
669+
<<"browse-error">>,
670+
[
671+
#{
672+
source => <<"Compiler">>,
673+
code => ErrorCode
674+
}
675+
]
676+
)
677+
}
678+
];
679+
browse_error(#{<<"source">> := <<"Elvis">>, <<"code">> := ErrorCode}) ->
680+
Title = <<"Browse: Elvis rules: ", ErrorCode/binary>>,
681+
[
682+
#{
683+
title => Title,
684+
kind => ?CODE_ACTION_KIND_BROWSE,
685+
command =>
686+
els_command:make_command(
687+
Title,
688+
<<"browse-error">>,
689+
[
690+
#{
691+
source => <<"Elvis">>,
692+
code => ErrorCode
693+
}
694+
]
695+
)
696+
}
697+
];
698+
browse_error(_Diagnostic) ->
699+
[].
700+
580701
-spec ensure_range(els_poi:poi_range(), binary(), [els_poi:poi()]) ->
581702
{ok, els_poi:poi_range()} | error.
582703
ensure_range(#{from := {Line, _}}, SubjectId, POIs) ->

apps/els_lsp/src/els_dt_references.erl

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
find_by/1,
2525
find_by_id/2,
2626
insert/2,
27-
versioned_insert/2
27+
versioned_insert/2,
28+
kind_to_category/1
2829
]).
2930

3031
%%==============================================================================

apps/els_lsp/src/els_execute_command_provider.erl

+77-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ options() ->
2626
<<"function-references">>,
2727
<<"refactor.extract">>,
2828
<<"add-behaviour-callbacks">>,
29-
<<"bump-variables">>
29+
<<"bump-variables">>,
30+
<<"browse-error">>,
31+
<<"browse-docs">>
3032
],
3133
#{
3234
commands => [
@@ -204,6 +206,54 @@ execute_command(<<"add-behaviour-callbacks">>, [
204206
els_server:send_request(Method, Params),
205207
[]
206208
end;
209+
execute_command(<<"browse-error">>, [#{<<"source">> := Source, <<"code">> := ErrorCodeBin}]) ->
210+
Url = make_url_browse_error(Source, ErrorCodeBin),
211+
launch_browser(Url);
212+
execute_command(<<"browse-docs">>, [
213+
#{
214+
<<"source">> := <<"otp">>,
215+
<<"app">> := App,
216+
<<"module">> := Module,
217+
<<"function">> := Function,
218+
<<"arity">> := Arity,
219+
<<"kind">> := Kind
220+
}
221+
]) ->
222+
Prefix =
223+
case Kind of
224+
<<"function">> -> "";
225+
<<"type">> -> "t:"
226+
end,
227+
Url = io_lib:format(
228+
"https://www.erlang.org/doc/apps/~s/~s.html#~s~s/~p",
229+
[App, Module, Prefix, Function, Arity]
230+
),
231+
%% TODO: Function
232+
launch_browser(Url);
233+
execute_command(<<"browse-docs">>, [
234+
#{
235+
<<"source">> := <<"hex">>,
236+
<<"app">> := App,
237+
<<"module">> := Module,
238+
<<"function">> := Function,
239+
<<"arity">> := Arity,
240+
<<"kind">> := Kind
241+
}
242+
]) ->
243+
%% Edoc uses #function-arity while ExDoc uses #function/arity
244+
%% We just support ExDoc for now.
245+
%% Suppose we could add special handling for known edoc apps.
246+
Prefix =
247+
case Kind of
248+
<<"function">> -> "";
249+
<<"type">> -> "t:"
250+
end,
251+
252+
Url = io_lib:format(
253+
"https://hexdocs.pm/~s/~s.html#~s~s/~p",
254+
[App, Module, Prefix, Function, Arity]
255+
),
256+
launch_browser(Url);
207257
execute_command(Command, Arguments) ->
208258
case wrangler_handler:execute_command(Command, Arguments) of
209259
true ->
@@ -216,6 +266,32 @@ execute_command(Command, Arguments) ->
216266
end,
217267
[].
218268

269+
-spec make_url_browse_error(binary(), binary()) -> string().
270+
make_url_browse_error(<<"Compiler">>, ErrorCodeBin) ->
271+
[Prefix | _] = ErrorCode = binary_to_list(ErrorCodeBin),
272+
"https://whatsapp.github.io/erlang-language-platform/" ++
273+
"docs/erlang-error-index/" ++
274+
string:lowercase([Prefix]) ++ "/" ++ ErrorCode ++ "/";
275+
make_url_browse_error(<<"Elvis">>, ErrorCodeBin) ->
276+
ErrorCode = binary_to_list(ErrorCodeBin),
277+
"https://github.com/inaka/elvis_core/blob/main/doc_rules/elvis_style/" ++
278+
ErrorCode ++ ".md".
279+
280+
-spec launch_browser(_) -> ok.
281+
launch_browser(Url) ->
282+
case os:type() of
283+
{win32, _} ->
284+
%% TODO: Not sure if this is the correct way to open a browser on Windows
285+
os:cmd("start " ++ Url);
286+
{_, linux} ->
287+
os:cmd("xdg-open " ++ Url);
288+
{_, darwin} ->
289+
os:cmd("open " ++ Url);
290+
{_, _} ->
291+
not_supported
292+
end,
293+
ok.
294+
219295
-spec bump_variables(uri(), range(), binary()) -> ok.
220296
bump_variables(Uri, Range, VarName) ->
221297
{Name, Number} = split_variable(VarName),

0 commit comments

Comments
 (0)