File Coverage

blib/lib/PLS/Server/Response/Completion.pm
Criterion Covered Total %
statement 39 204 19.1
branch 0 72 0.0
condition 0 23 0.0
subroutine 13 25 52.0
pod 0 9 0.0
total 52 333 15.6


line stmt bran cond sub pod time code
1             package PLS::Server::Response::Completion;
2              
3 9     9   54 use strict;
  9         9  
  9         226  
4 9     9   36 use warnings;
  9         18  
  9         191  
5              
6 9     9   36 use parent q(PLS::Server::Response);
  9         17  
  9         38  
7 9     9   431 use feature 'state';
  9         18  
  9         453  
8              
9 9     9   4523 use Pod::Functions;
  9         28505  
  9         1176  
10 9     9   54 use List::Util;
  9         19  
  9         427  
11 9     9   45 use Module::CoreList;
  9         18  
  9         80  
12 9     9   4423 use Module::Metadata;
  9         44833  
  9         280  
13 9     9   54 use ExtUtils::Installed;
  9         18  
  9         173  
14              
15 9     9   43 use PLS::Parser::Document;
  9         18  
  9         156  
16 9     9   44 use PLS::Parser::PackageSymbols;
  9         18  
  9         151  
17 9     9   37 use PLS::Parser::Pod;
  9         10  
  9         137  
18 9     9   37 use PLS::Server::State;
  9         17  
  9         19710  
19              
20             =head1 NAME
21              
22             PLS::Server::Response::Completion
23              
24             =head1 DESCRIPTION
25              
26             This is a message from the server to the client with a list
27             of completion items for the current location.
28              
29             =cut
30              
31             sub new
32             {
33 0     0 0   my ($class, $request) = @_;
34              
35 0           my $self = bless {id => $request->{id}, result => undef}, $class;
36              
37 0           my $document = PLS::Parser::Document->new(uri => $request->{params}{textDocument}{uri}, line => $request->{params}{position}{line});
38 0 0         return $self if (ref $document ne 'PLS::Parser::Document');
39              
40 0           my ($range, $arrow, $package, $filter) = $document->find_word_under_cursor(@{$request->{params}{position}}{qw(line character)});
  0            
41              
42 0 0         return $self if (ref $range ne 'HASH');
43              
44 0           $range->{start}{line} = $request->{params}{position}{line};
45 0           $range->{end}{line} = $request->{params}{position}{line};
46 0           $range->{end}{character} = $request->{params}{position}{character};
47              
48 0 0         $package =~ s/::$// if (length $package);
49              
50 0           my @results;
51 0           my $full_text = $document->get_full_text();
52              
53 0           my @futures;
54              
55 0 0         if ($filter =~ /^[\$\@\%]/)
56             {
57 0           push @results, @{get_variables($document, $filter, $full_text)};
  0            
58             }
59             else
60             {
61 0           my @this_document_packages;
62 0           my @packages = @{get_packages($document, $full_text, \@this_document_packages)};
  0            
63              
64 0 0         unless ($arrow)
65             {
66 0           push @results, @packages;
67 0           push @results, @{get_keywords()};
  0            
68             }
69              
70 0 0         if (length $package)
71             {
72 0           push @futures, get_package_functions($package, $filter, $arrow);
73             }
74              
75 0           push @results, @{get_subroutines($document, $arrow, $full_text, $this_document_packages[0])};
  0            
76              
77 0 0         if ($filter)
78             {
79 0           push @results, @{get_constants($document, $filter, $full_text)};
  0            
80              
81             # Imported functions can't be called with an arrow
82 0 0         push @futures, get_imported_package_functions($document, $full_text) unless $arrow;
83             } ## end if ($filter)
84              
85             } ## end else [ if ($filter =~ /^[\$\@\%]/...)]
86              
87 0           push @results, @{Future->wait_all(@futures)->then(
88             sub {
89 0     0     [map { @{$_->result} } grep { $_->is_ready } @_]
  0            
  0            
  0            
90             }
91 0           )->get()
92             };
93              
94 0           my %unique_by_detail;
95              
96 0           foreach my $result (@results)
97             {
98 0           my $new_text = $result->{label};
99 0 0         $new_text = $result->{insertText} if (length $result->{insertText});
100 0           delete $result->{insertText};
101 0 0 0       next if (exists $result->{detail} and length $result->{detail} and $unique_by_detail{$result->{detail}}++);
      0        
102              
103 0           push @{$self->{result}}, {%$result, textEdit => {newText => $new_text, range => $range}};
  0            
104             } ## end foreach my $result (@results...)
105              
106 0 0 0       if (not $arrow and not $package and $filter !~ /^\%\@/)
      0        
107             {
108 0           push @{$self->{result}}, get_snippets();
  0            
109             }
110              
111 0           return $self;
112             } ## end sub new
113              
114             sub get_keywords
115             {
116 0     0 0   state @keywords;
117              
118 0 0         return \@keywords if (scalar @keywords);
119              
120 0           my %seen_keywords;
121              
122 0           foreach my $family (keys %Pod::Functions::Kinds)
123             {
124 0           foreach my $sub (@{$Pod::Functions::Kinds{$family}})
  0            
125             {
126 0 0         next if $sub =~ /\s+/;
127 0 0         next if $seen_keywords{$sub}++;
128 0           push @keywords, {label => $sub, kind => 14};
129             } ## end foreach my $sub (@{$Pod::Functions::Kinds...})
130             } ## end foreach my $family (keys %Pod::Functions::Kinds...)
131              
132 0           foreach my $keyword (
133             qw(cmp continue default do else elsif eq for foreach ge given gt if le lock lt ne not or package sub unless until when while x xor -r -w -x -o -R -W -X -O -e -z -s -f -d -l -p -S -b -c -t -u -g -k -T -B -M -A -C)
134             )
135             {
136 0 0         next if $seen_keywords{$keyword}++;
137 0           push @keywords, {label => $keyword, kind => 14};
138             } ## end foreach my $keyword (...)
139              
140 0           return \@keywords;
141             } ## end sub get_keywords
142              
143             sub get_package_functions
144             {
145 0     0 0   my ($package, $filter, $arrow) = @_;
146              
147 0 0         return Future->done([]) unless (length $package);
148              
149             return PLS::Parser::PackageSymbols::get_package_symbols($PLS::Server::State::CONFIG, $package)->then(
150             sub {
151 0     0     my ($functions) = @_;
152              
153 0 0         return Future->done([]) if (ref $functions ne 'HASH');
154              
155 0 0         my $separator = $arrow ? '->' : '::';
156 0           my @functions;
157              
158 0           foreach my $package_name (keys %{$functions})
  0            
159             {
160 0           foreach my $name (@{$functions->{$package_name}})
  0            
161             {
162 0           my $fully_qualified = join $separator, $package_name, $name;
163              
164 0 0         my $result = {
165             label => $name,
166              
167             # If there is an arrow, we need to make sure to sort all the methods in this package to the top
168             sortText => $arrow ? "0000$name" : $fully_qualified,
169             kind => 3
170             };
171              
172 0 0         if ($arrow)
173             {
174 0           $result->{insertText} = $name;
175             }
176             else
177             {
178 0           $result->{insertText} = $fully_qualified;
179             }
180              
181 0 0         if ($arrow)
182             {
183 0           $result->{filterText} = $name;
184             }
185             else
186             {
187 0           $result->{filterText} = $fully_qualified;
188             }
189              
190 0           push @functions, $result;
191             } ## end foreach my $name (@{$functions...})
192             } ## end foreach my $package_name (keys...)
193              
194 0           return Future->done(\@functions);
195             }
196 0           );
197             } ## end sub get_package_functions
198              
199             sub get_imported_package_functions
200             {
201 0     0 0   my ($document, $full_text) = @_;
202              
203 0           my $imports = $document->get_imports($full_text);
204 0 0 0       return Future->done([]) if (ref $imports ne 'ARRAY' or not scalar @{$imports});
  0            
205              
206 0           return PLS::Parser::PackageSymbols::get_imported_package_symbols($PLS::Server::State::CONFIG, @{$imports})->then(
207             sub {
208 0     0     my ($imported_functions) = @_;
209              
210 0           my @results;
211 0           foreach my $package_name (keys %{$imported_functions})
  0            
212             {
213 0           foreach my $subroutine (@{$imported_functions->{$package_name}})
  0            
214             {
215 0           my $result = {
216             kind => 3,
217             label => $subroutine,
218             data => [$package_name],
219             detail => "${package_name}::${subroutine}",
220             };
221              
222             $result->{labelDetails} = {description => "${package_name}::${subroutine}"}
223 0 0         if $PLS::Server::State::CLIENT_CAPABILITIES->{textDocument}{completion}{completionItem}{labelDetailsSupport};
224 0           push @results, $result;
225             } ## end foreach my $subroutine (@{$imported_functions...})
226             } ## end foreach my $package_name (keys...)
227 0           return Future->done(\@results);
228             }
229 0           );
230             } ## end sub get_imported_package_functions
231              
232             sub get_subroutines
233             {
234 0     0 0   my ($document, $arrow, $full_text, $this_document_package) = @_;
235              
236 0           my %subroutines;
237              
238 0           foreach my $sub (@{$document->get_subroutines_fast($full_text)})
  0            
239             {
240 0 0         next if ($sub =~ /\n/);
241 0           $subroutines{$sub} = {label => $sub, kind => 3};
242 0 0         $subroutines{$sub}{data} = [$this_document_package] if (length $this_document_package);
243             } ## end foreach my $sub (@{$document...})
244              
245             # Add subroutines to the list, uniquifying and keeping track of the packages in which
246             # it is defined so that resolve can find the documentation.
247 0           foreach my $sub (keys %{$document->{index}->subs})
  0            
248             {
249 0           foreach my $data (@{$document->{index}->subs->{$sub}})
  0            
250             {
251 0   0       my $result = $subroutines{$sub} // {label => $sub, kind => $data->{kind}, data => []};
252              
253 0 0         if (length $data->{package})
254             {
255 0           push @{$result->{data}}, $data->{package};
  0            
256             }
257              
258 0           $subroutines{$sub} = $result;
259             } ## end foreach my $data (@{$document...})
260             } ## end foreach my $sub (keys %{$document...})
261              
262             # If the subroutine is only defined in one place, include the package name as the detail.
263 0           foreach my $sub (keys %subroutines)
264             {
265 0 0 0       if (exists $subroutines{$sub}{data} and ref $subroutines{$sub}{data} eq 'ARRAY' and scalar @{$subroutines{$sub}{data}} == 1)
  0   0        
266             {
267 0           $subroutines{$sub}{detail} = $subroutines{$sub}{data}[0] . "::${sub}";
268             }
269             } ## end foreach my $sub (keys %subroutines...)
270              
271 0           return [values %subroutines];
272             } ## end sub get_subroutines
273              
274             sub get_packages
275             {
276 0     0 0   my ($document, $full_text, $this_document_packages) = @_;
277              
278 0           my @packages;
279              
280 0           my $core_modules = PLS::Server::Cache::get_core_modules();
281 0           my $ext_modules = PLS::Server::Cache::get_ext_modules();
282              
283 0           @{$this_document_packages} = @{$document->get_packages_fast($full_text)};
  0            
  0            
284 0           push @packages, @{$this_document_packages};
  0            
285              
286 0           foreach my $pack (@{$core_modules}, @{$ext_modules})
  0            
  0            
287             {
288 0 0         next if ($pack =~ /\n/);
289 0           push @packages, $pack;
290             }
291              
292 0 0         if (ref $document->{index} eq 'PLS::Parser::Index')
293             {
294 0           push @packages, @{$document->{index}->get_all_packages()};
  0            
295             }
296              
297 0           return [map { {label => $_, kind => 7} } List::Util::uniq sort @packages];
  0            
298             } ## end sub get_packages
299              
300             sub get_constants
301             {
302 0     0 0   my ($document, $filter, $full_text) = @_;
303              
304 0           my %seen_constants;
305             my @constants;
306              
307 0           foreach my $constant (@{$document->get_constants_fast($full_text)})
  0            
308             {
309 0 0         next if $seen_constants{$constant}++;
310 0 0         next if ($constant =~ /\n/);
311 0           push @constants, {label => $constant, kind => 21};
312             } ## end foreach my $constant (@{$document...})
313              
314 0           return \@constants;
315             } ## end sub get_constants
316              
317             sub get_variables
318             {
319 0     0 0   my ($document, $full_text) = @_;
320              
321 0           my @variables;
322             my %seen_variables;
323              
324 0           foreach my $variable (@{PLS::Server::Cache::get_builtin_variables()}, @{$document->get_variables_fast($full_text)})
  0            
  0            
325             {
326 0 0         next if $seen_variables{$variable}++;
327 0 0         next if ($variable =~ /\n/);
328 0           push @variables,
329             {
330             label => $variable,
331             kind => 6
332             };
333              
334             # add other variable forms to the list for arrays and hashes
335 0 0         if ($variable =~ /^[\@\%]/)
336             {
337 0           my $name = $variable =~ s/^[\@\%]/\$/r;
338 0 0         my $append = $variable =~ /^\@/ ? '[' : '{';
339 0           push @variables,
340             {
341             label => $variable,
342             insertText => $name . $append,
343             filterText => $name,
344             kind => 6
345             };
346             } ## end if ($variable =~ /^[\@\%]/...)
347             } ## end foreach my $variable (@{PLS::Server::Cache::get_builtin_variables...})
348              
349 0           return \@variables;
350             } ## end sub get_variables
351              
352             sub get_snippets
353             {
354 0     0 0   state @snippets;
355              
356 0 0         return @snippets if (scalar @snippets);
357              
358 0           @snippets = (
359             {
360             label => 'sub',
361             detail => 'Insert subroutine',
362             kind => 15,
363             insertTextFormat => 2,
364             insertText => "sub \$1\n{\n\t\$0\n}",
365             },
366             {
367             label => 'foreach',
368             detail => 'Insert foreach loop',
369             kind => 15,
370             insertTextFormat => 2,
371             insertText => "foreach my \$1 (\$2)\n{\n\t\$0\n}",
372             },
373             {
374             label => 'for',
375             detail => 'Insert C-style for loop',
376             kind => 15,
377             insertTextFormat => 2,
378             insertText => "for (\$1 ; \$2 ; \$3)\n{\n\t\$0\n}",
379             },
380             {
381             label => 'while',
382             detail => 'Insert while statement',
383             kind => 15,
384             insertTextFormat => 2,
385             insertText => "while (\$1)\n{\n\t\$0\n}",
386             },
387             {
388             label => 'if',
389             detail => 'Insert if statement',
390             kind => 15,
391             insertTextFormat => 2,
392             insertText => "if (\$1)\n{\n\t\$0\n}",
393             },
394             {
395             label => 'elsif',
396             detail => 'Insert elsif statement',
397             kind => 15,
398             insertTextFormat => 2,
399             insertText => "elsif (\$1)\n{\n\t\$0\n}",
400             },
401             {
402             label => 'else',
403             detail => 'Insert else statement',
404             kind => 15,
405             insertTextFormat => 2,
406             insertText => "else\n{\n\t\$0\n}",
407             },
408             {
409             label => 'package',
410             detail => 'Create a new package',
411             kind => 15,
412             insertTextFormat => 2,
413             insertText => "package \$1;\n\nuse strict;\nuse warnings;\n\n\$0\n\n1;",
414             },
415             {
416             label => 'open my $fh, ...',
417             filterText => 'open',
418             sortText => 'open',
419             detail => 'Insert an open statement',
420             kind => 15,
421             insertTextFormat => 2,
422             insertText => q[open $1, '${2|<,>,>>,+<,+>,\|-,-\|,>&,<&=,>>&=|}', $3],
423             },
424             {
425             label => 'do { local $/; <$fh> }',
426             filterText => 'do',
427             sortText => 'do1',
428             detail => 'Slurp an entire filehandle',
429             kind => 15,
430             insertTextFormat => 2,
431             insertText => 'do { local $/; <$1> }'
432             },
433             {
434             label => 'while (my $line = <$fh>) { ... }',
435             filterText => 'while',
436             sortText => 'while1',
437             detail => 'Iterate through a filehandle line-by-line',
438             kind => 15,
439             insertTextFormat => 2,
440             insertText => "while (my \$1 = <\$2>)\n{\n\t\$0\n}"
441             },
442             {
443             label => 'my ($param1, $param2, ...) = @_;',
444             filterText => 'my',
445             sortText => 'my1',
446             detail => 'Get subroutine parameters',
447             kind => 15,
448             insertTextFormat => 2,
449             insertText => "my (\$1) = \@_;\n\n"
450             },
451             {
452             label => '$? >> 8',
453             filterText => '$?',
454             sortText => '$?',
455             detail => 'Get exit code',
456             kind => 15,
457             insertTextFormat => 2,
458             insertText => '? >> 8'
459             },
460             {
461             label => 'sort { $a <=> $b } ...',
462             filterText => 'sort',
463             sortText => 'sort1',
464             detail => 'Sort numerically ascending',
465             kind => 15,
466             insertTextFormat => 2,
467             insertText => 'sort { \$a <=> \$b } $1'
468             },
469             {
470             label => 'reverse sort { $a <=> $b } ...',
471             filterText => 'sort',
472             sortText => 'sort2',
473             detail => 'Sort numerically descending',
474             kind => 15,
475             insertTextFormat => 2,
476             insertText => 'reverse sort { \$a <=> \$b } $1'
477             }
478             );
479              
480 0           return @snippets;
481             } ## end sub get_snippets
482              
483             1;