Skip to content

Commit

Permalink
Introduce browse code actions
Browse files Browse the repository at this point in the history
* Browse elvis warnings
* Browse compiler errors
* Browse functions and types in otp docs or hex docs
  • Loading branch information
plux committed Oct 10, 2024
1 parent ed1daaa commit 06c89e3
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 34 deletions.
2 changes: 2 additions & 0 deletions apps/els_core/include/els_core.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,8 @@
%%------------------------------------------------------------------------------

-define(CODE_ACTION_KIND_QUICKFIX, <<"quickfix">>).
-define(CODE_ACTION_KIND_BROWSE, <<"browse">>).

-type code_action_kind() :: binary().

-type code_action_context() :: #{
Expand Down
18 changes: 17 additions & 1 deletion apps/els_core/src/els_uri.erl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
-export([
module/1,
path/1,
uri/1
uri/1,
app/1
]).

%%==============================================================================
Expand All @@ -26,6 +27,21 @@
%%==============================================================================
-include("els_core.hrl").

-spec app(uri() | [binary()]) -> {ok, atom()} | error.
app(Uri) when is_binary(Uri) ->
app(lists:reverse(filename:split(path(Uri))));
app([]) ->
error;
app([_File, <<"src">>, AppBin0 | _]) ->
case binary:split(AppBin0, <<"-">>) of
[AppBin, _Vsn] ->
{ok, binary_to_atom(AppBin)};
[AppBin] ->
{ok, binary_to_atom(AppBin)}
end;
app([_ | Rest]) ->
app(Rest).

-spec module(uri()) -> atom().
module(Uri) ->
binary_to_atom(filename:basename(path(Uri), <<".erl">>), utf8).
Expand Down
64 changes: 34 additions & 30 deletions apps/els_lsp/src/els_code_action_provider.erl
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ code_actions(Uri, Range, #{<<"diagnostics">> := Diagnostics}) ->
lists:flatten([make_code_actions(Uri, D) || D <- Diagnostics]) ++
wrangler_handler:get_code_actions(Uri, Range) ++
els_code_actions:extract_function(Uri, Range) ++
els_code_actions:bump_variables(Uri, Range)
els_code_actions:bump_variables(Uri, Range) ++
els_code_actions:browse_docs(Uri, Range)
).

-spec make_code_actions(uri(), map()) -> [map()].
Expand All @@ -43,35 +44,38 @@ make_code_actions(
#{<<"message">> := Message, <<"range">> := Range} = Diagnostic
) ->
Data = maps:get(<<"data">>, Diagnostic, <<>>),
make_code_actions(
[
{"function (.*) is unused", fun els_code_actions:export_function/4},
{"variable '(.*)' is unused", fun els_code_actions:ignore_variable/4},
{"variable '(.*)' is unbound", fun els_code_actions:suggest_variable/4},
{"undefined macro '(.*)'", fun els_code_actions:add_include_lib_macro/4},
{"undefined macro '(.*)'", fun els_code_actions:define_macro/4},
{"undefined macro '(.*)'", fun els_code_actions:suggest_macro/4},
{"record (.*) undefined", fun els_code_actions:add_include_lib_record/4},
{"record (.*) undefined", fun els_code_actions:define_record/4},
{"record (.*) undefined", fun els_code_actions:suggest_record/4},
{"field (.*) undefined in record (.*)", fun els_code_actions:suggest_record_field/4},
{"Module name '(.*)' does not match file name '(.*)'",
fun els_code_actions:fix_module_name/4},
{"Unused macro: (.*)", fun els_code_actions:remove_macro/4},
{"function (.*) undefined", fun els_code_actions:create_function/4},
{"function (.*) undefined", fun els_code_actions:suggest_function/4},
{"Cannot find definition for function (.*)", fun els_code_actions:suggest_function/4},
{"Cannot find module (.*)", fun els_code_actions:suggest_module/4},
{"Unused file: (.*)", fun els_code_actions:remove_unused/4},
{"Atom typo\\? Did you mean: (.*)", fun els_code_actions:fix_atom_typo/4},
{"undefined callback function (.*) \\\(behaviour '(.*)'\\\)",
fun els_code_actions:undefined_callback/4}
],
Uri,
Range,
Data,
Message
).
els_code_actions:browse_error(Diagnostic) ++
make_code_actions(
[
{"function (.*) is unused", fun els_code_actions:export_function/4},
{"variable '(.*)' is unused", fun els_code_actions:ignore_variable/4},
{"variable '(.*)' is unbound", fun els_code_actions:suggest_variable/4},
{"undefined macro '(.*)'", fun els_code_actions:add_include_lib_macro/4},
{"undefined macro '(.*)'", fun els_code_actions:define_macro/4},
{"undefined macro '(.*)'", fun els_code_actions:suggest_macro/4},
{"record (.*) undefined", fun els_code_actions:add_include_lib_record/4},
{"record (.*) undefined", fun els_code_actions:define_record/4},
{"record (.*) undefined", fun els_code_actions:suggest_record/4},
{"field (.*) undefined in record (.*)",
fun els_code_actions:suggest_record_field/4},
{"Module name '(.*)' does not match file name '(.*)'",
fun els_code_actions:fix_module_name/4},
{"Unused macro: (.*)", fun els_code_actions:remove_macro/4},
{"function (.*) undefined", fun els_code_actions:create_function/4},
{"function (.*) undefined", fun els_code_actions:suggest_function/4},
{"Cannot find definition for function (.*)",
fun els_code_actions:suggest_function/4},
{"Cannot find module (.*)", fun els_code_actions:suggest_module/4},
{"Unused file: (.*)", fun els_code_actions:remove_unused/4},
{"Atom typo\\? Did you mean: (.*)", fun els_code_actions:fix_atom_typo/4},
{"undefined callback function (.*) \\\(behaviour '(.*)'\\\)",
fun els_code_actions:undefined_callback/4}
],
Uri,
Range,
Data,
Message
).

-spec make_code_actions([{string(), Fun}], uri(), range(), binary(), binary()) ->
[map()]
Expand Down
129 changes: 128 additions & 1 deletion apps/els_lsp/src/els_code_actions.erl
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
suggest_record_field/4,
suggest_function/4,
suggest_module/4,
bump_variables/2
bump_variables/2,
browse_error/1,
browse_docs/2
]).

-include("els_lsp.hrl").
Expand Down Expand Up @@ -577,6 +579,131 @@ undefined_callback(Uri, _Range, _Data, [_Function, Behaviour]) ->
}
].

-spec browse_docs(uri(), range()) -> [map()].
browse_docs(Uri, Range) ->
#{from := {Line, Column}} = els_range:to_poi_range(Range),
{ok, Document} = els_utils:lookup_document(Uri),
POIs = els_dt_document:get_element_at_pos(Document, Line, Column),
lists:flatten([browse_docs(POI) || POI <- POIs]).

-spec browse_docs(els_poi:poi()) -> [map()].
browse_docs(#{id := {M, F, A}, kind := Kind}) when
Kind == application;
Kind == type_application
->
case els_utils:find_module(M) of
{ok, ModUri} ->
case els_uri:app(ModUri) of
{ok, App} ->
DocType = doc_type(ModUri),
make_browse_docs_command(DocType, {M, F, A}, App, Kind);
error ->
[]
end;
{error, not_found} ->
[]
end;
browse_docs(_) ->
[].

-spec doc_type(uri()) -> otp | hex | other.
doc_type(Uri) ->
Path = binary_to_list(els_uri:path(Uri)),
OtpPath = els_config:get(otp_path),
case lists:prefix(OtpPath, Path) of
true ->
otp;
false ->
IsDep = lists:any(
fun(DepPath) ->
lists:prefix(DepPath, Path)
end,
els_config:get(deps_paths)
),
case IsDep of
true ->
hex;
false ->
other
end
end.

-spec make_browse_docs_command(atom(), mfa(), atom(), atom()) ->
[map()].
make_browse_docs_command(other, _MFA, _App, _Kind) ->
[];
make_browse_docs_command(DocType, {M, F, A}, App, Kind) ->
Title = make_browse_docs_title(DocType, {M, F, A}),
[
#{
title => Title,
kind => ?CODE_ACTION_KIND_BROWSE,
command =>
els_command:make_command(
Title,
<<"browse-docs">>,
[
#{
source => DocType,
module => M,
function => F,
arity => A,
app => App,
kind => els_dt_references:kind_to_category(Kind)
}
]
)
}
].

-spec make_browse_docs_title(atom(), mfa()) -> binary().
make_browse_docs_title(otp, {M, F, A}) ->
list_to_binary(io_lib:format("Browse: OTP docs: ~p:~p/~p", [M, F, A]));
make_browse_docs_title(hex, {M, F, A}) ->
list_to_binary(io_lib:format("Browse: Hex docs: ~p:~p/~p", [M, F, A])).

-spec browse_error(map()) -> [map()].
browse_error(#{<<"source">> := <<"Compiler">>, <<"code">> := ErrorCode}) ->
Title = <<"Browse: Erlang Error Index: ", ErrorCode/binary>>,
[
#{
title => Title,
kind => ?CODE_ACTION_KIND_BROWSE,
command =>
els_command:make_command(
Title,
<<"browse-error">>,
[
#{
source => <<"Compiler">>,
code => ErrorCode
}
]
)
}
];
browse_error(#{<<"source">> := <<"Elvis">>, <<"code">> := ErrorCode}) ->
Title = <<"Browse: Elvis rules: ", ErrorCode/binary>>,
[
#{
title => Title,
kind => ?CODE_ACTION_KIND_BROWSE,
command =>
els_command:make_command(
Title,
<<"browse-error">>,
[
#{
source => <<"Elvis">>,
code => ErrorCode
}
]
)
}
];
browse_error(_Diagnostic) ->
[].

-spec ensure_range(els_poi:poi_range(), binary(), [els_poi:poi()]) ->
{ok, els_poi:poi_range()} | error.
ensure_range(#{from := {Line, _}}, SubjectId, POIs) ->
Expand Down
3 changes: 2 additions & 1 deletion apps/els_lsp/src/els_dt_references.erl
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
find_by/1,
find_by_id/2,
insert/2,
versioned_insert/2
versioned_insert/2,
kind_to_category/1
]).

%%==============================================================================
Expand Down
78 changes: 77 additions & 1 deletion apps/els_lsp/src/els_execute_command_provider.erl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ options() ->
<<"function-references">>,
<<"refactor.extract">>,
<<"add-behaviour-callbacks">>,
<<"bump-variables">>
<<"bump-variables">>,
<<"browse-error">>,
<<"browse-docs">>
],
#{
commands => [
Expand Down Expand Up @@ -204,6 +206,54 @@ execute_command(<<"add-behaviour-callbacks">>, [
els_server:send_request(Method, Params),
[]
end;
execute_command(<<"browse-error">>, [#{<<"source">> := Source, <<"code">> := ErrorCodeBin}]) ->
Url = make_url_browse_error(Source, ErrorCodeBin),
launch_browser(Url);
execute_command(<<"browse-docs">>, [
#{
<<"source">> := <<"otp">>,
<<"app">> := App,
<<"module">> := Module,
<<"function">> := Function,
<<"arity">> := Arity,
<<"kind">> := Kind
}
]) ->
Prefix =
case Kind of
<<"function">> -> "";
<<"type">> -> "t:"
end,
Url = io_lib:format(
"https://www.erlang.org/doc/apps/~s/~s.html#~s~s/~p",
[App, Module, Prefix, Function, Arity]
),
%% TODO: Function
launch_browser(Url);
execute_command(<<"browse-docs">>, [
#{
<<"source">> := <<"hex">>,
<<"app">> := App,
<<"module">> := Module,
<<"function">> := Function,
<<"arity">> := Arity,
<<"kind">> := Kind
}
]) ->
%% Edoc uses #function-arity while ExDoc uses #function/arity
%% We just support ExDoc for now.
%% Suppose we could add special handling for known edoc apps.
Prefix =
case Kind of
<<"function">> -> "";
<<"type">> -> "t:"
end,

Url = io_lib:format(
"https://hexdocs.pm/~s/~s.html#~s~s/~p",
[App, Module, Prefix, Function, Arity]
),
launch_browser(Url);
execute_command(Command, Arguments) ->
case wrangler_handler:execute_command(Command, Arguments) of
true ->
Expand All @@ -216,6 +266,32 @@ execute_command(Command, Arguments) ->
end,
[].

-spec make_url_browse_error(binary(), binary()) -> string().
make_url_browse_error(<<"Compiler">>, ErrorCodeBin) ->
[Prefix | _] = ErrorCode = binary_to_list(ErrorCodeBin),
"https://whatsapp.github.io/erlang-language-platform/" ++
"docs/erlang-error-index/" ++
string:lowercase([Prefix]) ++ "/" ++ ErrorCode ++ "/";
make_url_browse_error(<<"Elvis">>, ErrorCodeBin) ->
ErrorCode = binary_to_list(ErrorCodeBin),
"https://github.com/inaka/elvis_core/blob/main/doc_rules/elvis_style/" ++
ErrorCode ++ ".md".

-spec launch_browser(_) -> ok.
launch_browser(Url) ->
case os:type() of
{win32, _} ->
%% TODO: Not sure if this is the correct way to open a browser on Windows
os:cmd("start " ++ Url);
{_, linux} ->
os:cmd("xdg-open " ++ Url);
{_, darwin} ->
os:cmd("open " ++ Url);
{_, _} ->
not_supported
end,
ok.

-spec bump_variables(uri(), range(), binary()) -> ok.
bump_variables(Uri, Range, VarName) ->
{Name, Number} = split_variable(VarName),
Expand Down

0 comments on commit 06c89e3

Please sign in to comment.