Skip to content

Commit 52bf40e

Browse files
authored
Improvements to extract function (#1563)
* Ignore variables inside funs() and list comprehensions * Don't suggest to extract function unless it contains more than one poi
1 parent 5959282 commit 52bf40e

9 files changed

+182
-41
lines changed

apps/els_core/src/els_poi.erl

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
| include
4545
| include_lib
4646
| keyword_expr
47+
| list_comp
4748
| macro
4849
| module
4950
| nifs

apps/els_core/src/els_text.erl

+14-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
split_at_line/2,
1313
tokens/1,
1414
tokens/2,
15-
apply_edits/2
15+
apply_edits/2,
16+
is_keyword_expr/1
1617
]).
1718
-export([strip_comments/1]).
1819

@@ -176,6 +177,18 @@ strip_comments(Text) ->
176177
)
177178
).
178179

180+
-spec is_keyword_expr(binary()) -> boolean().
181+
is_keyword_expr(Text) ->
182+
lists:member(Text, [
183+
<<"begin">>,
184+
<<"case">>,
185+
<<"fun">>,
186+
<<"if">>,
187+
<<"maybe">>,
188+
<<"receive">>,
189+
<<"try">>
190+
]).
191+
179192
%%==============================================================================
180193
%% Internal functions
181194
%%==============================================================================

apps/els_lsp/priv/code_navigation/src/extract_function.erl

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ f(A, B) ->
1010
end,
1111
H = [X || X <- [A, B, C], X > 1],
1212
I = {A, B, A},
13-
ok.
13+
other_function(),
14+
[X || X <- [A, B, C], X > 1].
1415

1516
other_function() ->
1617
hello.

apps/els_lsp/src/els_code_actions.erl

+8-2
Original file line numberDiff line numberDiff line change
@@ -478,11 +478,17 @@ fix_atom_typo(Uri, Range, _Data, [Atom]) ->
478478
-spec extract_function(uri(), range()) -> [map()].
479479
extract_function(Uri, Range) ->
480480
{ok, [Document]} = els_dt_document:lookup(Uri),
481-
#{from := From = {Line, Column}, to := To} = els_range:to_poi_range(Range),
481+
PoiRange = els_range:to_poi_range(Range),
482+
#{from := From = {Line, Column}, to := To} = PoiRange,
482483
%% We only want to extract if selection is large enough
483484
%% and cursor is inside a function
485+
POIsInRange = els_dt_document:pois_in_range(Document, PoiRange),
486+
#{text := Text} = Document,
487+
MarkedText = els_text:range(Text, From, To),
484488
case
485-
large_enough_range(From, To) andalso
489+
(length(POIsInRange) > 1 orelse
490+
els_text:is_keyword_expr(MarkedText)) andalso
491+
large_enough_range(From, To) andalso
486492
not contains_function_clause(Document, Line) andalso
487493
els_dt_document:wrapping_functions(Document, Line, Column) /= []
488494
of

apps/els_lsp/src/els_dt_document.erl

+11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
new/4,
3131
pois/1,
3232
pois/2,
33+
pois_in_range/2,
3334
pois_in_range/3,
3435
get_element_at_pos/3,
3536
uri/1,
@@ -214,6 +215,16 @@ pois(#{pois := POIs}) ->
214215
pois(Item, Kinds) ->
215216
[POI || #{kind := K} = POI <- pois(Item), lists:member(K, Kinds)].
216217

218+
%% @doc Returns the list of POIs of the given types in the given range
219+
%% for the current document
220+
-spec pois_in_range(item(), els_poi:poi_range()) -> [els_poi:poi()].
221+
pois_in_range(Item, Range) ->
222+
[
223+
POI
224+
|| #{range := R} = POI <- pois(Item),
225+
els_range:in(R, Range)
226+
].
227+
217228
%% @doc Returns the list of POIs of the given types in the given range
218229
%% for the current document
219230
-spec pois_in_range(

apps/els_lsp/src/els_execute_command_provider.erl

+30-26
Original file line numberDiff line numberDiff line change
@@ -346,33 +346,49 @@ end_symbol(ExtractString) ->
346346
non_neg_integer()
347347
) -> [string()].
348348
get_args(PoiRange, Document, FromL, FunBeginLine) ->
349-
%% TODO: Possible improvement. To make this bullet proof we should
350-
%% ignore vars defined inside LCs and funs()
351-
VarPOIs = els_poi:sort(els_dt_document:pois(Document, [variable])),
352349
BeforeRange = #{from => {FunBeginLine, 1}, to => {FromL, 1}},
353-
VarsBefore = ids_in_range(BeforeRange, VarPOIs),
354-
VarsInside = ids_in_range(PoiRange, VarPOIs),
350+
VarPOIsBefore = els_dt_document:pois_in_range(Document, [variable], BeforeRange),
351+
%% Remove all variables inside LCs or keyword expressions
352+
LCPOIs = els_dt_document:pois(Document, [list_comp]),
353+
FunExprPOIs = [
354+
POI
355+
|| #{id := fun_expr} = POI <- els_dt_document:pois(Document, [keyword_expr])
356+
],
357+
%% Only consider fun exprs that doesn't contain the selected range
358+
ExcludePOIs = [
359+
POI
360+
|| #{range := R} = POI <- FunExprPOIs ++ LCPOIs, not els_range:in(PoiRange, R)
361+
],
362+
VarsBefore = [
363+
Id
364+
|| #{range := VarRange, id := Id} <- VarPOIsBefore,
365+
not_in_any_range(VarRange, ExcludePOIs)
366+
],
367+
%% Find all variables defined before the current function that are used
368+
%% inside the selected range.
369+
VarPOIsInside = els_dt_document:pois_in_range(Document, [variable], PoiRange),
355370
els_utils:uniq([
356371
atom_to_list(Id)
357-
|| Id <- VarsInside,
372+
|| #{id := Id} <- els_poi:sort(VarPOIsInside),
358373
lists:member(Id, VarsBefore)
359374
]).
360375

361-
-spec ids_in_range(els_poi:poi_range(), [els_poi:poi()]) -> [atom()].
362-
ids_in_range(PoiRange, VarPOIs) ->
363-
[
364-
Id
365-
|| #{range := R, id := Id} <- VarPOIs,
366-
els_range:in(R, PoiRange)
367-
].
376+
-spec not_in_any_range(els_poi:poi_range(), [els_poi:poi()]) -> boolean().
377+
not_in_any_range(VarRange, POIs) ->
378+
not lists:any(
379+
fun(#{range := Range}) ->
380+
els_range:in(VarRange, Range)
381+
end,
382+
POIs
383+
).
368384

369385
-spec extract_range(els_dt_document:item(), range()) -> els_poi:poi_range().
370386
extract_range(#{text := Text} = Document, Range) ->
371387
PoiRange = els_range:to_poi_range(Range),
372388
#{from := {CurrL, CurrC} = From, to := To} = PoiRange,
373389
POIs = els_dt_document:get_element_at_pos(Document, CurrL, CurrC),
374390
MarkedText = els_text:range(Text, From, To),
375-
case is_keyword_expr(MarkedText) of
391+
case els_text:is_keyword_expr(MarkedText) of
376392
true ->
377393
case sort_by_range_size([P || #{kind := keyword_expr} = P <- POIs]) of
378394
[] ->
@@ -384,18 +400,6 @@ extract_range(#{text := Text} = Document, Range) ->
384400
PoiRange
385401
end.
386402

387-
-spec is_keyword_expr(binary()) -> boolean().
388-
is_keyword_expr(Text) ->
389-
lists:member(Text, [
390-
<<"begin">>,
391-
<<"case">>,
392-
<<"fun">>,
393-
<<"if">>,
394-
<<"maybe">>,
395-
<<"receive">>,
396-
<<"try">>
397-
]).
398-
399403
-spec sort_by_range_size(_) -> _.
400404
sort_by_range_size(POIs) ->
401405
lists:sort([{range_size(P), P} || P <- POIs]).

apps/els_lsp/src/els_parser.erl

+2-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,8 @@ do_points_of_interest(Tree) ->
276276
Type == implicit_fun;
277277
Type == maybe_expr;
278278
Type == receive_expr;
279-
Type == try_expr
279+
Type == try_expr;
280+
Type == fun_expr
280281
->
281282
keyword_expr(Type, Tree);
282283
_Other ->

apps/els_lsp/test/els_code_action_SUITE.erl

+74-1
Original file line numberDiff line numberDiff line change
@@ -472,27 +472,100 @@ fix_callbacks(Config) ->
472472
extract_function(Config) ->
473473
Uri = ?config(extract_function_uri, Config),
474474
%% These shouldn't return any code actions
475+
%% -export([f/2]).
476+
%% ^^^^^
475477
#{result := []} = els_client:document_codeaction(
476478
Uri,
477479
els_protocol:range(#{from => {2, 1}, to => {2, 5}}),
478480
[]
479481
),
482+
%% <empty line>
480483
#{result := []} = els_client:document_codeaction(
481484
Uri,
482485
els_protocol:range(#{from => {3, 1}, to => {3, 5}}),
483486
[]
484487
),
488+
%% f(A, B) ->
489+
%% ^^^^^
485490
#{result := []} = els_client:document_codeaction(
486491
Uri,
487492
els_protocol:range(#{from => {4, 1}, to => {4, 5}}),
488493
[]
489494
),
495+
%% C = 1,
496+
%% ^
490497
#{result := []} = els_client:document_codeaction(
491498
Uri,
492499
els_protocol:range(#{from => {5, 8}, to => {5, 9}}),
493500
[]
494501
),
502+
%% other_function()
503+
%% ^^^^^^^^^^^^^^^^
504+
#{result := []} = els_client:document_codeaction(
505+
Uri,
506+
els_protocol:range(#{from => {13, 4}, to => {13, 20}}),
507+
[]
508+
),
509+
%% This should return a code action
510+
%% F = A + B + C,
511+
%% ^^^^^^^^^
512+
#{
513+
result := [
514+
#{
515+
command := #{
516+
title := <<"Extract function">>,
517+
arguments := [#{uri := Uri}]
518+
},
519+
kind := <<"refactor.extract">>,
520+
title := <<"Extract function">>
521+
}
522+
]
523+
} = els_client:document_codeaction(
524+
Uri,
525+
els_protocol:range(#{from => {6, 8}, to => {6, 17}}),
526+
[]
527+
),
528+
%% This should return a code action
529+
%% G = case A of
530+
%% ^^^^
531+
#{
532+
result := [
533+
#{
534+
command := #{
535+
title := <<"Extract function">>,
536+
arguments := [#{uri := Uri}]
537+
},
538+
kind := <<"refactor.extract">>,
539+
title := <<"Extract function">>
540+
}
541+
]
542+
} = els_client:document_codeaction(
543+
Uri,
544+
els_protocol:range(#{from => {7, 9}, to => {7, 13}}),
545+
[]
546+
),
547+
%% This should return a code action
548+
%% H = [X || X <- [A, B, C], X > 1],
549+
%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
550+
#{
551+
result := [
552+
#{
553+
command := #{
554+
title := <<"Extract function">>,
555+
arguments := [#{uri := Uri}]
556+
},
557+
kind := <<"refactor.extract">>,
558+
title := <<"Extract function">>
559+
}
560+
]
561+
} = els_client:document_codeaction(
562+
Uri,
563+
els_protocol:range(#{from => {11, 8}, to => {11, 36}}),
564+
[]
565+
),
495566
%% This should return a code action
567+
%% I = {A, B, A},
568+
%% ^^^^^^^^^
496569
#{
497570
result := [
498571
#{
@@ -506,7 +579,7 @@ extract_function(Config) ->
506579
]
507580
} = els_client:document_codeaction(
508581
Uri,
509-
els_protocol:range(#{from => {5, 8}, to => {5, 17}}),
582+
els_protocol:range(#{from => {12, 8}, to => {12, 17}}),
510583
[]
511584
),
512585
ok.

apps/els_lsp/test/els_execute_command_SUITE.erl

+40-9
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
suggest_spec/1,
1919
extract_function/1,
2020
extract_function_case/1,
21-
extract_function_tuple/1
21+
extract_function_tuple/1,
22+
extract_function_list_comp/1
2223
]).
2324

2425
%%==============================================================================
@@ -57,7 +58,8 @@ init_per_testcase(TestCase, Config0) when
5758
TestCase =:= ct_run_test;
5859
TestCase =:= extract_function;
5960
TestCase =:= extract_function_case;
60-
TestCase =:= extract_function_tuple
61+
TestCase =:= extract_function_tuple;
62+
TestCase =:= extract_function_list_comp
6163
->
6264
Config = els_test_utils:init_per_testcase(TestCase, Config0),
6365
setup_mocks(),
@@ -82,7 +84,8 @@ end_per_testcase(TestCase, Config) when
8284
TestCase =:= ct_run_test;
8385
TestCase =:= extract_function;
8486
TestCase =:= extract_function_case;
85-
TestCase =:= extract_function_tuple
87+
TestCase =:= extract_function_tuple;
88+
TestCase =:= extract_function_list_comp
8689
->
8790
teardown_mocks(),
8891
els_test_utils:end_per_testcase(TestCase, Config);
@@ -285,8 +288,8 @@ extract_function(Config) ->
285288
" A + B + C.\n\n"
286289
>>,
287290
range := #{
288-
'end' := #{character := 0, line := 14},
289-
start := #{character := 0, line := 14}
291+
'end' := #{character := 0, line := 15},
292+
start := #{character := 0, line := 15}
290293
}
291294
}
292295
] = Changes.
@@ -315,12 +318,40 @@ extract_function_case(Config) ->
315318
>>,
316319
range :=
317320
#{
318-
'end' := #{character := 0, line := 14},
319-
start := #{character := 0, line := 14}
321+
'end' := #{character := 0, line := 15},
322+
start := #{character := 0, line := 15}
320323
}
321324
}
322325
] = Changes.
323326

327+
-spec extract_function_list_comp(config()) -> ok.
328+
extract_function_list_comp(Config) ->
329+
Uri = ?config(extract_function_uri, Config),
330+
execute_command_refactor_extract(Uri, {13, 4}, {13, 32}),
331+
[#{edit := #{changes := #{Uri := Changes}}}] = get_edits_from_meck_history(),
332+
[
333+
#{
334+
range :=
335+
#{
336+
start := #{line := 13, character := 4},
337+
'end' := #{line := 13, character := 32}
338+
},
339+
newText := <<"new_function(A, B, C)">>
340+
},
341+
#{
342+
range :=
343+
#{
344+
start := #{line := 15, character := 0},
345+
'end' := #{line := 15, character := 0}
346+
},
347+
newText :=
348+
<<
349+
"new_function(A, B, C) ->\n"
350+
" [X || X <- [A, B, C], X > 1].\n\n"
351+
>>
352+
}
353+
] = Changes.
354+
324355
-spec extract_function_tuple(config()) -> ok.
325356
extract_function_tuple(Config) ->
326357
Uri = ?config(extract_function_uri, Config),
@@ -343,8 +374,8 @@ extract_function_tuple(Config) ->
343374
>>,
344375
range :=
345376
#{
346-
'end' := #{character := 0, line := 14},
347-
start := #{character := 0, line := 14}
377+
'end' := #{character := 0, line := 15},
378+
start := #{character := 0, line := 15}
348379
}
349380
}
350381
] = Changes.

0 commit comments

Comments
 (0)