diff --git a/apps/els_core/include/els_core.hrl b/apps/els_core/include/els_core.hrl index 94f45047a..a9cb862d0 100644 --- a/apps/els_core/include/els_core.hrl +++ b/apps/els_core/include/els_core.hrl @@ -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() :: #{ diff --git a/apps/els_core/src/els_uri.erl b/apps/els_core/src/els_uri.erl index 7aea6941f..14966829d 100644 --- a/apps/els_core/src/els_uri.erl +++ b/apps/els_core/src/els_uri.erl @@ -11,7 +11,8 @@ -export([ module/1, path/1, - uri/1 + uri/1, + app/1 ]). %%============================================================================== @@ -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). diff --git a/apps/els_lsp/src/els_code_action_provider.erl b/apps/els_lsp/src/els_code_action_provider.erl index 2f0a4286a..a2631f0a2 100644 --- a/apps/els_lsp/src/els_code_action_provider.erl +++ b/apps/els_lsp/src/els_code_action_provider.erl @@ -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()]. @@ -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()] diff --git a/apps/els_lsp/src/els_code_actions.erl b/apps/els_lsp/src/els_code_actions.erl index c4e8fb5d8..d969db569 100644 --- a/apps/els_lsp/src/els_code_actions.erl +++ b/apps/els_lsp/src/els_code_actions.erl @@ -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"). @@ -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) -> diff --git a/apps/els_lsp/src/els_dt_references.erl b/apps/els_lsp/src/els_dt_references.erl index ada5228bb..17dbf3000 100644 --- a/apps/els_lsp/src/els_dt_references.erl +++ b/apps/els_lsp/src/els_dt_references.erl @@ -24,7 +24,8 @@ find_by/1, find_by_id/2, insert/2, - versioned_insert/2 + versioned_insert/2, + kind_to_category/1 ]). %%============================================================================== diff --git a/apps/els_lsp/src/els_execute_command_provider.erl b/apps/els_lsp/src/els_execute_command_provider.erl index 014ada2a8..3d261dded 100644 --- a/apps/els_lsp/src/els_execute_command_provider.erl +++ b/apps/els_lsp/src/els_execute_command_provider.erl @@ -26,7 +26,9 @@ options() -> <<"function-references">>, <<"refactor.extract">>, <<"add-behaviour-callbacks">>, - <<"bump-variables">> + <<"bump-variables">>, + <<"browse-error">>, + <<"browse-docs">> ], #{ commands => [ @@ -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 -> @@ -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),