@@ -24,6 +24,7 @@ options() ->
24
24
<<" show-behaviour-usages" >>,
25
25
<<" suggest-spec" >>,
26
26
<<" function-references" >>,
27
+ <<" refactor.extract" >>,
27
28
<<" add-behaviour-callbacks" >>
28
29
],
29
30
#{
@@ -106,6 +107,14 @@ execute_command(<<"suggest-spec">>, [
106
107
},
107
108
els_server :send_request (Method , Params ),
108
109
[];
110
+ execute_command (<<" refactor.extract" >>, [
111
+ #{
112
+ <<" uri" >> := Uri ,
113
+ <<" range" >> := Range
114
+ }
115
+ ]) ->
116
+ ok = extract_function (Uri , Range ),
117
+ [];
109
118
execute_command (<<" add-behaviour-callbacks" >>, [
110
119
#{
111
120
<<" uri" >> := Uri ,
@@ -197,6 +206,137 @@ execute_command(Command, Arguments) ->
197
206
end ,
198
207
[].
199
208
209
+ -spec extract_function (uri (), range ()) -> ok .
210
+ extract_function (Uri , Range ) ->
211
+ {ok , [#{text := Text } = Document ]} = els_dt_document :lookup (Uri ),
212
+ ExtractRange = extract_range (Document , Range ),
213
+ #{from := {FromL , FromC } = From , to := {ToL , ToC }} = ExtractRange ,
214
+ ExtractString0 = els_text :range (Text , From , {ToL , ToC }),
215
+ % % Trim whitespace
216
+ ExtractString = string :trim (ExtractString0 , both , " \n\r\t " ),
217
+ % % Trim trailing termination symbol
218
+ ExtractStringTrimmed = string :trim (ExtractString , trailing , " ,.;" ),
219
+ Method = <<" workspace/applyEdit" >>,
220
+ case els_dt_document :wrapping_functions (Document , FromL , FromC ) of
221
+ [WrappingFunPOI | _ ] when ExtractStringTrimmed /= <<>> ->
222
+ % % WrappingFunPOI is the function that we are currently in
223
+ #{
224
+ data := #{
225
+ wrapping_range :=
226
+ #{
227
+ from := {FunBeginLine , _ },
228
+ to := {FunEndLine , _ }
229
+ }
230
+ }
231
+ } = WrappingFunPOI ,
232
+ % % Get args needed for the new function
233
+ Args = get_args (ExtractRange , Document , FromL , FunBeginLine ),
234
+ ArgsBin = unicode :characters_to_binary (string :join (Args , " , " )),
235
+ FunClause = <<" new_function(" , ArgsBin /binary , " )" >>,
236
+ % % Place the new function after the current function
237
+ EndSymbol = end_symbol (ExtractString ),
238
+ NewRange = els_protocol :range (
239
+ #{from => {FunEndLine + 1 , 1 }, to => {FunEndLine + 1 , 1 }}
240
+ ),
241
+ FunBody = unicode :characters_to_list (
242
+ <<FunClause /binary , " ->\n " , ExtractStringTrimmed /binary , " ." >>
243
+ ),
244
+ {ok , FunBodyFormatted , _ } = erlfmt :format_string (FunBody , []),
245
+ NewFun = unicode :characters_to_binary (FunBodyFormatted ++ " \n " ),
246
+ Changes = [
247
+ #{
248
+ newText => <<FunClause /binary , EndSymbol /binary >>,
249
+ range => els_protocol :range (ExtractRange )
250
+ },
251
+ #{
252
+ newText => NewFun ,
253
+ range => NewRange
254
+ }
255
+ ],
256
+ Params = #{edit => #{changes => #{Uri => Changes }}},
257
+ els_server :send_request (Method , Params );
258
+ _ ->
259
+ ? LOG_INFO (" No wrapping function found" ),
260
+ ok
261
+ end .
262
+
263
+ -spec end_symbol (binary ()) -> binary ().
264
+ end_symbol (ExtractString ) ->
265
+ case binary :last (ExtractString ) of
266
+ $. -> <<" ." >>;
267
+ $, -> <<" ," >>;
268
+ $; -> <<" ;" >>;
269
+ _ -> <<>>
270
+ end .
271
+
272
+ % % @doc Find all variables defined in the function before the current.
273
+ % % If they are used inside the selected range, they need to be
274
+ % % sent in as arguments to the new function.
275
+ -spec get_args (
276
+ els_poi :poi_range (),
277
+ els_dt_document :item (),
278
+ non_neg_integer (),
279
+ non_neg_integer ()
280
+ ) -> [string ()].
281
+ get_args (PoiRange , Document , FromL , FunBeginLine ) ->
282
+ % % TODO: Possible improvement. To make this bullet proof we should
283
+ % % ignore vars defined inside LCs and funs()
284
+ VarPOIs = els_poi :sort (els_dt_document :pois (Document , [variable ])),
285
+ BeforeRange = #{from => {FunBeginLine , 1 }, to => {FromL , 1 }},
286
+ VarsBefore = ids_in_range (BeforeRange , VarPOIs ),
287
+ VarsInside = ids_in_range (PoiRange , VarPOIs ),
288
+ els_utils :uniq ([
289
+ atom_to_list (Id )
290
+ || Id <- VarsInside ,
291
+ lists :member (Id , VarsBefore )
292
+ ]).
293
+
294
+ -spec ids_in_range (els_poi :poi_range (), [els_poi :poi ()]) -> [atom ()].
295
+ ids_in_range (PoiRange , VarPOIs ) ->
296
+ [
297
+ Id
298
+ || #{range := R , id := Id } <- VarPOIs ,
299
+ els_range :in (R , PoiRange )
300
+ ].
301
+
302
+ -spec extract_range (els_dt_document :item (), range ()) -> els_poi :poi_range ().
303
+ extract_range (#{text := Text } = Document , Range ) ->
304
+ PoiRange = els_range :to_poi_range (Range ),
305
+ #{from := {CurrL , CurrC } = From , to := To } = PoiRange ,
306
+ POIs = els_dt_document :get_element_at_pos (Document , CurrL , CurrC ),
307
+ MarkedText = els_text :range (Text , From , To ),
308
+ case is_keyword_expr (MarkedText ) of
309
+ true ->
310
+ case sort_by_range_size ([P || #{kind := keyword_expr } = P <- POIs ]) of
311
+ [] ->
312
+ PoiRange ;
313
+ [{_Size , #{range := SmallestRange }} | _ ] ->
314
+ SmallestRange
315
+ end ;
316
+ false ->
317
+ PoiRange
318
+ end .
319
+
320
+ -spec is_keyword_expr (binary ()) -> boolean ().
321
+ is_keyword_expr (Text ) ->
322
+ lists :member (Text , [
323
+ <<" begin" >>,
324
+ <<" case" >>,
325
+ <<" fun" >>,
326
+ <<" if" >>,
327
+ <<" maybe" >>,
328
+ <<" receive" >>,
329
+ <<" try" >>
330
+ ]).
331
+
332
+ -spec sort_by_range_size (_ ) -> _ .
333
+ sort_by_range_size (POIs ) ->
334
+ lists :sort ([{range_size (P ), P } || P <- POIs ]).
335
+
336
+ -spec range_size (_ ) -> _ .
337
+ range_size (#{range := #{from := {FromL , FromC }, to := {ToL , ToC }}}) ->
338
+ {ToL - FromL , ToC - FromC }.
339
+
200
340
-spec spec_text (binary ()) -> binary ().
201
341
spec_text (<<" -callback" , Rest /binary >>) ->
202
342
<<" -spec" , Rest /binary >>;
0 commit comments