File Coverage

blib/lib/Complete/Module.pm
Criterion Covered Total %
statement 68 68 100.0
branch 31 34 91.1
condition 33 35 94.2
subroutine 8 8 100.0
pod 1 1 100.0
total 141 146 96.5


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