Skip to content
This repository was archived by the owner on Aug 15, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
12 changes: 11 additions & 1 deletion apps/els_core/src/els_config.erl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
initialize/4,
get/1,
set/2,
start_link/0
start_link/0,
is_dep/1
]).

%% gen_server callbacks
Expand Down Expand Up @@ -584,6 +585,15 @@ expand_var(Bin, [{Var, Value} | RestEnv]) ->
[Value, RestBin]
end.

-spec is_dep(string()) -> boolean().
is_dep(Path) ->
lists:any(
fun(DepPath) ->
lists:prefix(DepPath, Path)
end,
els_config:get(deps_paths)
).

-spec get_env() -> [{string(), string()}].
-if(?OTP_RELEASE >= 24).
get_env() ->
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
10 changes: 10 additions & 0 deletions apps/els_lsp/priv/code_navigation/src/code_action_browse_docs.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-module(code_action_browse_docs).

-spec function_a(file:filename()) -> pid().
function_e(L) ->
lists:sort(L),
self().

-spec function_b() -> my_dep_mod:my_type().
function_f() ->
my_dep_mod:my_function().
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
123 changes: 122 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,125 @@ 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 ->
case els_config:is_dep(Path) 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
Loading
Loading