File Coverage

blib/lib/Complete/Module.pm
Criterion Covered Total %
statement 77 86 89.5
branch 32 38 84.2
condition 33 38 86.8
subroutine 10 10 100.0
pod 1 1 100.0
total 153 173 88.4


line stmt bran cond sub pod time code
1             package Complete::Module;
2              
3 2     2   2079 use 5.010001;
  2         7  
4 2     2   13 use strict;
  2         3  
  2         43  
5 2     2   8 use warnings;
  2         4  
  2         44  
6 2     2   3646 use Log::ger;
  2         98  
  2         10  
7              
8 2     2   1398 use Complete::Common qw(:all);
  2         806  
  2         298  
9 2     2   14 use Exporter qw(import);
  2         14  
  2         52  
10 2     2   14 use List::Util qw(uniq);
  2         12  
  2         2829  
11              
12             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
13             our $DATE = '2023-03-19'; # DATE
14             our $DIST = 'Complete-Module'; # DIST
15             our $VERSION = '0.263'; # VERSION
16              
17             our %SPEC;
18              
19             our @EXPORT_OK = qw(complete_module);
20              
21             our $OPT_SHORTCUT_PREFIXES;
22             if ($ENV{COMPLETE_MODULE_OPT_SHORTCUT_PREFIXES}) {
23             $OPT_SHORTCUT_PREFIXES =
24             { split /=|;/, $ENV{COMPLETE_MODULE_OPT_SHORTCUT_PREFIXES} };
25             } else {
26             $OPT_SHORTCUT_PREFIXES = {
27             #cp => 'Catalyst/Plugin/' # candidate
28             df => 'DateTime/Format/',
29             #dp => 'Dancer/Plugin/', # candidate
30             #d2p => 'Dancer2/Plugin/', # candidate
31             dz => 'Dist/Zilla/',
32             dzb => 'Dist/Zilla/PluginBundle/',
33             dzp => 'Dist/Zilla/Plugin/',
34             dzr => 'Dist/Zilla/Role/',
35             #pa => 'Plack/App/', # candidate
36             #pc => 'POE/Component/', # candidate
37             #pc => 'Perl/Critic/', # candidate?
38             #pcp => 'Perl/Critic/Policy/', # candidate?
39             #pd => 'Padre/Document/', # candidate
40             #pm => 'Plack/Middleware/', # candidate
41             #pp => 'Padre/Plugin/', # candidate
42             pw => 'Pod/Weaver/',
43             pwb => 'Pod/Weaver/PluginBundle/',
44             pwp => 'Pod/Weaver/Plugin/',
45             pwr => 'Pod/Weaver/Role/',
46             pws => 'Pod/Weaver/Section/',
47             #rtx => 'RT/Extension/', # candidate
48             #se => 'Search/Elasticsearch/', # candidate
49             #sec => 'Search/Elasticsearch/Client/', # candidate
50             #ser => 'Search/Elasticsearch/Role/', # candidate
51             #tp => 'Template/Plugin/', # candidate
52             #tw => 'Tickit/Widget/', # candidate
53              
54             # MooseX, MooX
55             # Moose::Exception
56             # Finance::Bank
57             # Mojo::*, MojoX::*, Mojolicious::*
58             };
59             }
60              
61             $SPEC{complete_module} = {
62             v => 1.1,
63             summary => 'Complete with installed Perl module names',
64             description => <<'_',
65              
66             For each directory in `@INC` (coderefs are ignored), find Perl modules and
67             module prefixes which have `word` as prefix. So for example, given `Te` as
68             `word`, will return e.g. `[Template, Template::, Term::, Test, Test::, Text::]`.
69             Given `Text::` will return `[Text::ASCIITable, Text::Abbrev, ...]` and so on.
70              
71             This function has a bit of overlapping functionality with , but
72             this function is geared towards shell tab completion. Compared to Module::List,
73             here are some differences: 1) list modules where prefix is incomplete; 2)
74             interface slightly different; 3) (currently) doesn't do recursing; 4) contains
75             conveniences for completion, e.g. map casing, expand intermediate paths (see
76             `Complete` for more details on those features), autoselection of path separator
77             character, some shortcuts, and so on.
78              
79             _
80             args => {
81             %arg_word,
82             path_sep => {
83             summary => 'Path separator',
84             schema => 'str*',
85             description => <<'_',
86              
87             For convenience in shell (bash) completion, instead of defaulting to `::` all
88             the time, will look at `word`. If word does not contain any `::` then will
89             default to `/`. This is because `::` (contains colon) is rather problematic as
90             it is by default a word-break character in bash and the word needs to be quoted
91             to avoid word-breaking by bash.
92              
93             _
94             },
95             find_pm => {
96             summary => 'Whether to find .pm files',
97             schema => 'bool*',
98             default => 1,
99             },
100             find_pod => {
101             summary => 'Whether to find .pod files',
102             schema => 'bool*',
103             default => 1,
104             },
105             find_pmc => {
106             summary => 'Whether to find .pmc files',
107             schema => 'bool*',
108             default => 1,
109             },
110             find_prefix => {
111             summary => 'Whether to find module prefixes',
112             schema => 'bool*',
113             default => 1,
114             },
115             ns_prefix => {
116             summary => 'Namespace prefix',
117             schema => 'perl::modname*',
118             description => <<'_',
119              
120             This is useful if you want to complete module under a specific namespace
121             (instead of the root). For example, if you set `ns_prefix` to
122             `Dist::Zilla::Plugin` (or `Dist::Zilla::Plugin::`) and word is `F`, you can get
123             `['FakeRelease', 'FileFinder::', 'FinderCode']` (those are modules under the
124             `Dist::Zilla::Plugin::` namespace).
125              
126             _
127             },
128             ns_prefixes => {
129             summary => 'Namespace prefixes',
130             schema => ['array*', of=>'perl::modname*'],
131             description => <<'_',
132              
133             If you specify this instead of `ns_prefix`, then the routine will search from
134             all the prefixes instead of just one.
135              
136             _
137             },
138             recurse => {
139             schema => 'bool*',
140             cmdline_aliases => {r=>{}},
141             },
142             recurse_matching => {
143             schema => ['str*', in=>['level-by-level', 'all-at-once']],
144             default => 'level-by-level',
145             },
146             exclude_leaf => {
147             schema => 'bool*',
148             },
149             exclude_dir => {
150             schema => 'bool*',
151             },
152             },
153             args_rels => {
154             choose_one => [qw/ns_prefix ns_prefixes/],
155             },
156             result_naked => 1,
157             };
158             sub complete_module {
159 22     22 1 123683 require Complete::Path;
160              
161 22         63 my %args = @_;
162              
163 22   50     64 my $word = $args{word} // '';
164             #log_trace('[compmod] Entering complete_module(), word=<%s>', $word);
165             #log_trace('[compmod] args=%s', \%args);
166              
167             # convenience: allow Foo/Bar.{pm,pod,pmc}
168 22         51 $word =~ s/\.(pm|pmc|pod)\z//;
169              
170             # convenience (and compromise): if word doesn't contain :: we use the
171             # "safer" separator /, but if already contains '::' we use '::'. (Can also
172             # use '.' if user uses that.) Using "::" in bash means user needs to use
173             # quote (' or ") to make completion behave as expected since : is by default
174             # a word break character in bash/readline.
175 22         70 my $sep = $args{path_sep};
176 22 50       63 unless (defined $sep) {
177 22 50       86 $sep = $word =~ /::/ ? '::' :
    100          
178             $word =~ /\./ ? '.' : '/';
179             }
180              
181             # find shortcut prefixes
182             {
183 22         33 my $tmp = lc $word;
  22         44  
184 22         86 for (keys %$OPT_SHORTCUT_PREFIXES) {
185 211 100       2381 if ($tmp =~ /\A\Q$_\E(?:(\Q$sep\E).*|\z)/) {
186             substr($word, 0, length($_) + length($1 // '')) =
187 1   50     10 $OPT_SHORTCUT_PREFIXES->{$_};
188 1         4 last;
189             }
190             }
191             }
192              
193 22         152 $word =~ s!(::|/|\.)!::!g;
194              
195 22   100     93 my $find_pm = $args{find_pm} // 1;
196 22   100     71 my $find_pmc = $args{find_pmc} // 1;
197 22   100     59 my $find_pod = $args{find_pod} // 1;
198 22   100     58 my $find_prefix = $args{find_prefix} // 1;
199              
200 22 100       63 my @ns_prefixes = $args{ns_prefixes} ? @{$args{ns_prefixes}} : ($args{ns_prefix});
  2         6  
201 22         39 my $res = [];
202 22         40 for my $ns_prefix (@ns_prefixes) {
203 26   100     82 $ns_prefix //= '';
204 26         54 $ns_prefix =~ s/(::)+\z//;
205              
206             #log_trace('[compmod] invoking complete_path, word=<%s>', $word);
207             my $cp_res = Complete::Path::complete_path(
208             word => $word,
209             starting_path => $ns_prefix,
210             list_func => sub {
211 68     68   23678 my ($path, $intdir, $isint) = @_;
212 68         218 (my $fspath = $path) =~ s!::!/!g;
213 68         108 my @res;
214 68         128 for my $inc (@INC) {
215 748 50       1963 next if ref($inc);
216 748 100       2039 my $dir = $inc . (length($fspath) ? "/$fspath" : "");
217 748 100       13379 opendir my($dh), $dir or next;
218 228         9728 for (readdir $dh) {
219 5140 100 100     15735 next if $_ eq '.' || $_ eq '..';
220 4684 100       19795 next unless /\A\w+(\.\w+)?\z/;
221 4460         51022 my $is_dir = (-d "$dir/$_");
222 4460 100 100     18543 next if $isint && !$is_dir;
223 2577 100 100     10268 push @res, "$_\::" if $is_dir && ($isint || $find_prefix);
      100        
224 2577 100 100     6274 push @res, $1 if /(.+)\.pm\z/ && $find_pm;
225 2577 100 100     4760 push @res, $1 if /(.+)\.pmc\z/ && $find_pmc;
226 2577 100 100     7763 push @res, $1 if /(.+)\.pod\z/ && $find_pod;
227             }
228             }
229 68         2191 [sort(uniq(@res))];
230             },
231             path_sep => '::',
232       28     is_dir_func => sub { }, # not needed, we already suffix "dirs" with ::
233             recurse => $args{recurse},
234             recurse_matching => $args{recurse_matching},
235             exclude_leaf => $args{exclude_leaf},
236             exclude_dir => $args{exclude_dir},
237 26         271 );
238 26         4021 push @$res, @$cp_res;
239             } # for $ns_prefix
240              
241             # dedup
242             {
243 22 100       39 last unless @ns_prefixes > 1;
  22         68  
244 2         3 my $res_dedup = [];
245 2         4 my %seen;
246 2 100       5 for (@$res) { push @$res_dedup, $_ unless $seen{$_}++ }
  6         19  
247 2         6 $res = $res_dedup;
248             }
249              
250             FILTER_WITH_SCHEMA: {
251 22         39 my $sch = $args{_schema};
  22         43  
252 22 50       53 last unless $sch;
253              
254 0         0 my $fres = [];
255 0         0 for my $word (@$res) {
256 0         0 log_trace("[compmod] Validating word %s with validator ...", $word);
257 0 0 0     0 if ($sch->[1]{in} && !(grep { index($_, $word)==0 } @{ $sch->[1]{in} })) {
  0         0  
  0         0  
258 0         0 next;
259             }
260 0         0 push @$fres, $word;
261             }
262 0         0 $res = $fres;
263             }
264              
265 22         45 for (@$res) { s/::/$sep/g }
  48         146  
266              
267 22         71 $res = { words=>$res, path_sep=>$sep };
268             #log_trace('[compmod] Leaving complete_module(), result=<%s>', $res);
269 22         90 $res;
270             }
271              
272             1;
273             # ABSTRACT: Complete with installed Perl module names
274              
275             __END__