diff --git a/apps/els_lsp/priv/code_navigation/src/undefined_record_suggest.erl b/apps/els_lsp/priv/code_navigation/src/undefined_record_suggest.erl new file mode 100644 index 00000000..a01ee33b --- /dev/null +++ b/apps/els_lsp/priv/code_navigation/src/undefined_record_suggest.erl @@ -0,0 +1,7 @@ +-module(undefined_record_suggest). + +-record(foobar, {foobar}). + +function_a(R) -> + #foo_bar{} = R, + R#foobar.foobaz. diff --git a/apps/els_lsp/src/els_code_action_provider.erl b/apps/els_lsp/src/els_code_action_provider.erl index 0b6e074b..53e3d868 100644 --- a/apps/els_lsp/src/els_code_action_provider.erl +++ b/apps/els_lsp/src/els_code_action_provider.erl @@ -52,6 +52,8 @@ make_code_actions( {"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}, diff --git a/apps/els_lsp/src/els_code_actions.erl b/apps/els_lsp/src/els_code_actions.erl index 87898264..6476d976 100644 --- a/apps/els_lsp/src/els_code_actions.erl +++ b/apps/els_lsp/src/els_code_actions.erl @@ -14,7 +14,9 @@ define_record/4, add_include_lib_macro/4, add_include_lib_record/4, - suggest_macro/4 + suggest_macro/4, + suggest_record/4, + suggest_record_field/4 ]). -include("els_lsp.hrl"). @@ -294,6 +296,54 @@ suggest_macro(Uri, Range, _Data, [Macro]) -> Distance > 0.8 ]. +-spec suggest_record(uri(), range(), binary(), [binary()]) -> [map()]. +suggest_record(Uri, Range, _Data, [Record]) -> + %% Supply a quickfix to replace an unrecognized record with the most similar + %% record in scope. + {ok, Document} = els_utils:lookup_document(Uri), + POIs = els_scope:local_and_included_pois(Document, [record]), + RecordsInScope = [atom_to_binary(Id) || #{id := Id} <- POIs, is_atom(Id)], + Distances = + [{els_utils:jaro_distance(Rec, Record), Rec} || Rec <- RecordsInScope, Rec =/= Record], + [ + make_edit_action( + Uri, + <<"Did you mean #", Rec/binary, "{}?">>, + ?CODE_ACTION_KIND_QUICKFIX, + <<"#", Rec/binary>>, + Range + ) + || {Distance, Rec} <- lists:reverse(lists:usort(Distances)), + Distance > 0.8 + ]. + +-spec suggest_record_field(uri(), range(), binary(), [binary()]) -> [map()]. +suggest_record_field(Uri, Range, _Data, [Field, Record]) -> + %% Supply a quickfix to replace an unrecognized record field with the most + %% similar record field in Record. + {ok, Document} = els_utils:lookup_document(Uri), + POIs = els_scope:local_and_included_pois(Document, [record]), + RecordId = binary_to_atom(Record, utf8), + Fields = [ + atom_to_binary(F) + || #{id := Id, data := #{field_list := Fs}} <- POIs, + F <- Fs, + Id =:= RecordId + ], + Distances = + [{els_utils:jaro_distance(F, Field), F} || F <- Fields, F =/= Field], + [ + make_edit_action( + Uri, + <<"Did you mean #", Record/binary, ".", F/binary, "?">>, + ?CODE_ACTION_KIND_QUICKFIX, + <>, + Range + ) + || {Distance, F} <- lists:reverse(lists:usort(Distances)), + Distance > 0.8 + ]. + -spec fix_module_name(uri(), range(), binary(), [binary()]) -> [map()]. fix_module_name(Uri, Range0, _Data, [ModName, FileName]) -> {ok, Document} = els_utils:lookup_document(Uri), diff --git a/apps/els_lsp/test/els_code_action_SUITE.erl b/apps/els_lsp/test/els_code_action_SUITE.erl index 7d48e55a..1e1a51bd 100644 --- a/apps/els_lsp/test/els_code_action_SUITE.erl +++ b/apps/els_lsp/test/els_code_action_SUITE.erl @@ -27,7 +27,8 @@ define_macro/1, define_macro_with_args/1, suggest_macro/1, - undefined_record/1 + undefined_record/1, + undefined_record_suggest/1 ]). %%============================================================================== @@ -736,3 +737,58 @@ undefined_record(Config) -> ], ?assertEqual(Expected, Result), ok. + +-spec undefined_record_suggest(config()) -> ok. +undefined_record_suggest(Config) -> + Uri = ?config(undefined_record_suggest_uri, Config), + %% Don't care + Range = els_protocol:range(#{ + from => {5, 4}, + to => {5, 11} + }), + DestRange = els_protocol:range(#{ + from => {4, 1}, + to => {4, 1} + }), + Diag = #{ + message => <<"record foo_bar undefined">>, + range => Range, + severity => 2, + source => <<"Compiler">> + }, + #{result := Result} = + els_client:document_codeaction(Uri, Range, [Diag]), + Changes1 = + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => Range, + newText => <<"#foobar">> + } + ] + }, + Changes2 = + #{ + binary_to_atom(Uri, utf8) => + [ + #{ + range => DestRange, + newText => <<"-record(foo_bar, {}).\n">> + } + ] + }, + Expected = [ + #{ + edit => #{changes => Changes1}, + kind => <<"quickfix">>, + title => <<"Did you mean #foobar{}?">> + }, + #{ + edit => #{changes => Changes2}, + kind => <<"quickfix">>, + title => <<"Define record foo_bar">> + } + ], + ?assertEqual(Expected, Result), + ok.