Skip to content

Commit

Permalink
Add support for renaming modules (erlang-ls#1196)
Browse files Browse the repository at this point in the history
  • Loading branch information
plux committed Feb 14, 2022
1 parent 6154388 commit e0939ff
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 29 deletions.
16 changes: 16 additions & 0 deletions apps/els_lsp/priv/code_navigation/src/rename_module_a.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-module(rename_module_a).

-export([ function_a/0
, function_b/0
]).
-export_type([type_a/0]).

-type type_a() :: any().

-callback function_a() -> type_a().

function_a() ->
a.

function_b() ->
b.
14 changes: 14 additions & 0 deletions apps/els_lsp/priv/code_navigation/src/rename_module_b.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-module(rename_module_b).

-behaviour(rename_module_a).
-import(rename_module_a, [function_b/0]).

-export([function_a/0]).

-type type_a() :: rename_module_a:type_a().

-spec function_a() -> type_a().
function_a() ->
rename_module_a:function_a(),
F = fun rename_module_a:function_a/0,
F().
48 changes: 31 additions & 17 deletions apps/els_lsp/src/els_parser.erl
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,11 @@ application(Tree) ->
ModFunTree = erl_syntax:application_operator(Tree),
Pos = erl_syntax:get_pos(ModFunTree),
FunTree = erl_syntax:module_qualifier_body(ModFunTree),
[poi(Pos, application, MFA,
#{name_range => els_range:range(erl_syntax:get_pos(FunTree))})]
ModTree = erl_syntax:module_qualifier_argument(ModFunTree),
Data = #{ name_range => els_range:range(erl_syntax:get_pos(FunTree))
, mod_range => els_range:range(erl_syntax:get_pos(ModTree))
},
[poi(Pos, application, MFA, Data)]
end.

-spec application_mfa(tree()) ->
Expand Down Expand Up @@ -276,7 +279,8 @@ attribute(Tree) ->
AttrName =:= behavior ->
case is_atom_node(Arg) of
{true, Behaviour} ->
[poi(Pos, behaviour, Behaviour)];
Data = #{mod_range => els_range:range(erl_syntax:get_pos(Arg))},
[poi(Pos, behaviour, Behaviour, Data)];
false ->
[]
end;
Expand Down Expand Up @@ -306,9 +310,9 @@ attribute(Tree) ->
find_export_pois(Tree, AttrName, Arg);
{import, [ModTree, ImportList]} ->
case is_atom_node(ModTree) of
{true, M} ->
{true, _} ->
Imports = erl_syntax:list_elements(ImportList),
find_import_entry_pois(M, Imports);
find_import_entry_pois(ModTree, Imports);
_ ->
[]
end;
Expand Down Expand Up @@ -436,14 +440,17 @@ find_export_entry_pois(EntryPoiKind, Exports) ->
|| FATree <- Exports
]).

-spec find_import_entry_pois(atom(), [tree()]) -> [poi()].
find_import_entry_pois(M, Imports) ->
-spec find_import_entry_pois(tree(), [tree()]) -> [poi()].
find_import_entry_pois(ModTree, Imports) ->
M = erl_syntax:atom_value(ModTree),
lists:flatten(
[ case get_name_arity(FATree) of
{F, A} ->
FTree = erl_syntax:arity_qualifier_body(FATree),
poi(erl_syntax:get_pos(FATree), import_entry, {M, F, A},
#{name_range => els_range:range(erl_syntax:get_pos(FTree))});
Data = #{ name_range => els_range:range(erl_syntax:get_pos(FTree))
, mod_range => els_range:range(erl_syntax:get_pos(ModTree))
},
poi(erl_syntax:get_pos(FATree), import_entry, {M, F, A}, Data);
false ->
[]
end
Expand Down Expand Up @@ -556,16 +563,20 @@ implicit_fun(Tree) ->
undefined -> [];
_ ->
NameTree = erl_syntax:implicit_fun_name(Tree),
FunTree =
Data =
case FunSpec of
{_, _, _} ->
erl_syntax:arity_qualifier_body(
erl_syntax:module_qualifier_body(NameTree));
ModTree = erl_syntax:module_qualifier_argument(NameTree),
FunTree = erl_syntax:arity_qualifier_body(
erl_syntax:module_qualifier_body(NameTree)),
#{ name_range => els_range:range(erl_syntax:get_pos(FunTree))
, mod_range => els_range:range(erl_syntax:get_pos(ModTree))
};
{_, _} ->
erl_syntax:arity_qualifier_body(NameTree)
FunTree = erl_syntax:arity_qualifier_body(NameTree),
#{name_range => els_range:range(erl_syntax:get_pos(FunTree))}
end,
[poi(erl_syntax:get_pos(Tree), implicit_fun, FunSpec,
#{name_range => els_range:range(erl_syntax:get_pos(FunTree))})]
[poi(erl_syntax:get_pos(Tree), implicit_fun, FunSpec, Data)]
end.

-spec macro(tree()) -> [poi()].
Expand Down Expand Up @@ -727,8 +738,11 @@ type_application(Tree) ->
ModTypeTree = erl_syntax:type_application_name(Tree),
Pos = erl_syntax:get_pos(ModTypeTree),
TypeTree = erl_syntax:module_qualifier_body(ModTypeTree),
[poi(Pos, type_application, Id,
#{name_range => els_range:range(erl_syntax:get_pos(TypeTree))})];
ModTree = erl_syntax:module_qualifier_argument(ModTypeTree),
Data = #{ name_range => els_range:range(erl_syntax:get_pos(TypeTree))
, mod_range => els_range:range(erl_syntax:get_pos(ModTree))
},
[poi(Pos, type_application, Id, Data)];
{Name, Arity} when Type =:= user_type_application ->
%% user-defined local type
Id = {Name, Arity},
Expand Down
34 changes: 24 additions & 10 deletions apps/els_lsp/src/els_references_provider.erl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
%% For use in other providers
-export([ find_references/2
, find_scoped_references_for_def/2
, find_references_to_module/1
]).

%%==============================================================================
Expand Down Expand Up @@ -105,16 +106,8 @@ find_references(Uri, Poi = #{kind := Kind})
find_scoped_references_for_def(Uri, Poi))
end;
find_references(Uri, #{kind := module}) ->
case els_utils:lookup_document(Uri) of
{ok, Doc} ->
Exports = els_dt_document:pois(Doc, [export_entry]),
ExcludeLocalRefs =
fun(Loc) ->
maps:get(uri, Loc) =/= Uri
end,
Refs = lists:flatmap(fun(E) -> find_references(Uri, E) end, Exports),
lists:filter(ExcludeLocalRefs, Refs)
end;
Refs = find_references_to_module(Uri),
[location(U, R) || #{uri := U, range := R} <- Refs];
find_references(_Uri, #{kind := Kind, id := Name})
when Kind =:= behaviour ->
find_references_for_id(Kind, Name);
Expand All @@ -141,6 +134,27 @@ kind_to_ref_kinds(type_definition) ->
kind_to_ref_kinds(Kind) ->
[Kind].

-spec find_references_to_module(uri()) -> [els_dt_references:item()].
find_references_to_module(Uri) ->
M = els_uri:module(Uri),
{ok, Doc} = els_utils:lookup_document(Uri),
ExportRefs =
lists:flatmap(
fun(#{id := {F, A}}) ->
{ok, Rs} =
els_dt_references:find_by_id(export_entry, {M, F, A}),
Rs
end, els_dt_document:pois(Doc, [export_entry])),
ExportTypeRefs =
lists:flatmap(
fun(#{id := {F, A}}) ->
{ok, Rs} =
els_dt_references:find_by_id(export_type_entry, {M, F, A}),
Rs
end, els_dt_document:pois(Doc, [export_type_entry])),
{ok, BehaviourRefs} = els_dt_references:find_by_id(behaviour, M),
ExcludeLocalRefs = fun(Loc) -> maps:get(uri, Loc) =/= Uri end,
lists:filter(ExcludeLocalRefs, ExportRefs ++ ExportTypeRefs ++ BehaviourRefs).

-spec find_references_for_id(poi_kind(), any()) -> [location()].
find_references_for_id(Kind, Id) ->
Expand Down
46 changes: 44 additions & 2 deletions apps/els_lsp/src/els_rename_provider.erl
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,34 @@ handle_request({rename, Params}, State) ->
-spec workspace_edits(uri(), [poi()], binary()) -> null | [any()].
workspace_edits(_Uri, [], _NewName) ->
null;
workspace_edits(OldUri, [#{kind := module} = POI| _], NewName) ->
%% Generate new Uri
Path = els_uri:path(OldUri),
Dir = filename:dirname(Path),
NewPath = filename:join(Dir, <<NewName/binary, ".erl">>),
NewUri = els_uri:uri(NewPath),
%% Find references that needs to be changed
Refs = els_references_provider:find_references_to_module(OldUri),
RefPOIs = convert_references_to_pois(Refs, [ application
, implicit_fun
, import_entry
, type_application
, behaviour
]),
Changes = [#{ textDocument => #{uri => RefUri}
, edits => [#{ range => editable_range(RefPOI, module)
, newText => NewName
}]
} || {RefUri, RefPOI} <- RefPOIs],
#{documentChanges =>
[ %% Update -module attribute
#{textDocument => #{uri => OldUri},
edits => [change(POI, NewName)]
}
%% Rename file
, #{kind => rename, oldUri => OldUri, newUri => NewUri}
| Changes]
};
workspace_edits(Uri, [#{kind := function_clause} = POI| _], NewName) ->
#{id := {F, A, _}} = POI,
#{changes => changes(Uri, POI#{kind => function, id => {F, A}}, NewName)};
Expand Down Expand Up @@ -112,7 +140,18 @@ workspace_edits(_Uri, _POIs, _NewName) ->
null.

-spec editable_range(poi()) -> range().
editable_range(#{kind := Kind, data := #{name_range := Range}})
editable_range(POI) ->
editable_range(POI, function).

-spec editable_range(poi(), function | module) -> range().
editable_range(#{kind := Kind, data := #{mod_range := Range}}, module)
when Kind =:= application;
Kind =:= implicit_fun;
Kind =:= import_entry;
Kind =:= type_application;
Kind =:= behaviour ->
els_protocol:range(Range);
editable_range(#{kind := Kind, data := #{name_range := Range}}, function)
when Kind =:= application;
Kind =:= implicit_fun;
Kind =:= callback;
Expand All @@ -126,10 +165,13 @@ editable_range(#{kind := Kind, data := #{name_range := Range}})
%% type_application POI of a built-in type don't have name_range data
%% they are handled by the next clause
els_protocol:range(Range);
editable_range(#{kind := _Kind, range := Range}) ->
editable_range(#{kind := _Kind, range := Range}, _) ->
els_protocol:range(Range).


-spec changes(uri(), poi(), binary()) -> #{uri() => [text_edit()]} | null.
changes(Uri, #{kind := module} = Mod, NewName) ->
#{Uri => [#{range => editable_range(Mod), newText => NewName}]};
changes(Uri, #{kind := variable} = Var, NewName) ->
POIs = els_code_navigation:find_in_scope(Uri, Var),
#{Uri => [#{range => editable_range(P), newText => NewName} || P <- POIs]};
Expand Down
38 changes: 38 additions & 0 deletions apps/els_lsp/test/els_rename_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
%% Test cases
-export([ rename_behaviour_callback/1
, rename_macro/1
, rename_module/1
, rename_variable/1
, rename_function/1
, rename_function_quoted_atom/1
Expand Down Expand Up @@ -220,6 +221,43 @@ rename_macro(Config) ->
},
assert_changes(Expected, Result).

-spec rename_module(config()) -> ok.
rename_module(Config) ->
UriA = ?config(rename_module_a_uri, Config),
UriB = ?config(rename_module_b_uri, Config),
NewName = <<"new_module">>,
Path = filename:dirname(els_uri:path(UriA)),
NewUri = els_uri:uri(filename:join(Path, <<NewName/binary, ".erl">>)),
#{result := #{documentChanges := Result}} =
els_client:document_rename(UriA, 0, 14, NewName),
Expected = [
%% Module attribute
#{ edits => [change(NewName, {0, 8}, {0, 23})]
, textDocument => #{uri => UriA}}
%% Rename file
, #{ kind => <<"rename">>
, newUri => NewUri
, oldUri => UriA}
%% Implicit function
, #{ edits => [change(NewName, {12, 10}, {12, 25})]
, textDocument => #{uri => UriB}}
%% Function application
, #{ edits => [change(NewName, {11, 2}, {11, 17})]
, textDocument => #{uri => UriB}}
%% Import
, #{ edits => [change(NewName, {3, 8}, {3, 23})]
, textDocument => #{uri => UriB}}
%% Type application
, #{ edits => [change(NewName, {7, 18}, {7, 33})]
, textDocument => #{uri => UriB}}
%% Behaviour
, #{ edits => [change(NewName, {2, 11}, {2, 26})]
, textDocument => #{uri => UriB}}
],
?assertEqual([], Result -- Expected),
?assertEqual([], Expected -- Result),
?assertEqual(lists:sort(Expected), lists:sort(Result)).

-spec rename_function(config()) -> ok.
rename_function(Config) ->
Uri = ?config(rename_function_uri, Config),
Expand Down

0 comments on commit e0939ff

Please sign in to comment.