Skip to content

Commit 06c89e3

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 06c89e3

6 files changed

+260
-34
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_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

+128-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,131 @@ 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+
IsDep = lists:any(
618+
fun(DepPath) ->
619+
lists:prefix(DepPath, Path)
620+
end,
621+
els_config:get(deps_paths)
622+
),
623+
case IsDep of
624+
true ->
625+
hex;
626+
false ->
627+
other
628+
end
629+
end.
630+
631+
-spec make_browse_docs_command(atom(), mfa(), atom(), atom()) ->
632+
[map()].
633+
make_browse_docs_command(other, _MFA, _App, _Kind) ->
634+
[];
635+
make_browse_docs_command(DocType, {M, F, A}, App, Kind) ->
636+
Title = make_browse_docs_title(DocType, {M, F, A}),
637+
[
638+
#{
639+
title => Title,
640+
kind => ?CODE_ACTION_KIND_BROWSE,
641+
command =>
642+
els_command:make_command(
643+
Title,
644+
<<"browse-docs">>,
645+
[
646+
#{
647+
source => DocType,
648+
module => M,
649+
function => F,
650+
arity => A,
651+
app => App,
652+
kind => els_dt_references:kind_to_category(Kind)
653+
}
654+
]
655+
)
656+
}
657+
].
658+
659+
-spec make_browse_docs_title(atom(), mfa()) -> binary().
660+
make_browse_docs_title(otp, {M, F, A}) ->
661+
list_to_binary(io_lib:format("Browse: OTP docs: ~p:~p/~p", [M, F, A]));
662+
make_browse_docs_title(hex, {M, F, A}) ->
663+
list_to_binary(io_lib:format("Browse: Hex docs: ~p:~p/~p", [M, F, A])).
664+
665+
-spec browse_error(map()) -> [map()].
666+
browse_error(#{<<"source">> := <<"Compiler">>, <<"code">> := ErrorCode}) ->
667+
Title = <<"Browse: Erlang Error Index: ", ErrorCode/binary>>,
668+
[
669+
#{
670+
title => Title,
671+
kind => ?CODE_ACTION_KIND_BROWSE,
672+
command =>
673+
els_command:make_command(
674+
Title,
675+
<<"browse-error">>,
676+
[
677+
#{
678+
source => <<"Compiler">>,
679+
code => ErrorCode
680+
}
681+
]
682+
)
683+
}
684+
];
685+
browse_error(#{<<"source">> := <<"Elvis">>, <<"code">> := ErrorCode}) ->
686+
Title = <<"Browse: Elvis rules: ", ErrorCode/binary>>,
687+
[
688+
#{
689+
title => Title,
690+
kind => ?CODE_ACTION_KIND_BROWSE,
691+
command =>
692+
els_command:make_command(
693+
Title,
694+
<<"browse-error">>,
695+
[
696+
#{
697+
source => <<"Elvis">>,
698+
code => ErrorCode
699+
}
700+
]
701+
)
702+
}
703+
];
704+
browse_error(_Diagnostic) ->
705+
[].
706+
580707
-spec ensure_range(els_poi:poi_range(), binary(), [els_poi:poi()]) ->
581708
{ok, els_poi:poi_range()} | error.
582709
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)