File Coverage

blib/lib/PLS/Server/Response/Completion.pm
Criterion Covered Total %
statement 39 225 17.3
branch 0 84 0.0
condition 0 26 0.0
subroutine 13 26 50.0
pod 0 10 0.0
total 52 371 14.0


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