File Coverage

blib/lib/Open/This.pm
Criterion Covered Total %
statement 147 168 87.5
branch 95 112 84.8
condition 23 35 65.7
subroutine 19 22 86.3
pod 4 4 100.0
total 288 341 84.4


line stmt bran cond sub pod time code
1 9     9   6770 use strict;
  9         72  
  9         329  
2 9     9   49 use warnings;
  9         19  
  9         793  
3             package Open::This;
4              
5             our $VERSION = '0.000031';
6              
7             our @ISA = qw(Exporter);
8             our @EXPORT_OK = qw(
9             maybe_get_url_from_parsed_text
10             editor_args_from_parsed_text
11             parse_text
12             to_editor_args
13             );
14              
15 9         54 use Module::Runtime qw(
16             is_module_name
17             module_notional_filename
18             require_module
19 9     9   4635 );
  9         16896  
20 9     9   4560 use Module::Util ();
  9         28443  
  9         307  
21 9     9   8061 use Path::Tiny qw( path );
  9         153820  
  9         586  
22 9     9   4759 use Try::Tiny qw( catch try );
  9         19297  
  9         510  
23 9     9   4819 use URI ();
  9         41395  
  9         18973  
24              
25             ## no critic (Subroutines::ProhibitExplicitReturnUndef)
26              
27             sub parse_text {
28 42     42 1 23578 my $text = join q{ }, @_;
29              
30 42 50       115 if ($text) {
31 42         364 $text =~ s/^\s+|\s+$//g;
32             }
33              
34             # Don't fail on a trailing colon which was accidentally pasted.
35 42 50       103 if ($text) {
36 42         124 $text =~ s{:\z}{};
37             }
38              
39 42 50       141 return undef if !$text;
40 42         133 my %parsed = ( original_text => $text );
41              
42 42         117 my ( $line, $col ) = _maybe_extract_line_number( \$text );
43 42         109 $parsed{line_number} = $line;
44 42 100       99 $parsed{column_number} = $col if $col;
45 42         110 $parsed{sub_name} = _maybe_extract_subroutine_name( \$text );
46              
47 42         128 _maybe_filter_text( \$text );
48              
49             # Is this an actual file.
50 42 100       138 if ( -e path($text) ) {
    100          
    100          
51 23         1588 $parsed{file_name} = $text;
52             }
53             elsif ( my $file_name = _maybe_git_diff_path($text) ) {
54 1         3 $parsed{file_name} = $file_name;
55             }
56             elsif ( $text =~ m{^[^/]+\.go$} ) {
57 2         9 my $file_name = _find_go_files($text);
58 2 100       9 $parsed{file_name} = $file_name if $file_name;
59             }
60              
61 42 100       203 if ( !exists $parsed{file_name} ) {
62 17 100       44 if ( my $bin = _which($text) ) {
63 1         33 $parsed{file_name} = $bin;
64 1         4 $text = $bin;
65             }
66             }
67              
68 42         138 $parsed{is_module_name} = is_module_name($text);
69              
70 42 100 100     811 if ( !$parsed{file_name} && $text =~ m{\Ahttps?://}i ) {
71 4         10 $parsed{file_name} = _maybe_extract_file_from_url( \$text );
72             }
73              
74 42 100 100     136 if ( !$parsed{file_name} && $parsed{is_module_name} ) {
75 7         20 $parsed{file_name} = _maybe_find_local_file($text);
76             }
77              
78             # This is a loadable module. Have this come after the local module checks
79             # so that we don't default to installed modules.
80 42 100 100     179 if ( !$parsed{file_name} && $parsed{is_module_name} ) {
81 4         26 my $found = Module::Util::find_installed($text);
82 4 100       1883 if ($found) {
83 2         8 $parsed{file_name} = $found;
84             }
85             }
86              
87 42 100       104 if ( !$parsed{line_number} ) {
88             $parsed{line_number} = _maybe_extract_line_number_via_sub_name(
89             $parsed{file_name},
90             $parsed{sub_name}
91 23         72 );
92             }
93              
94 118         330 my %return = map { $_ => $parsed{$_} }
95 42 100       163 grep { defined $parsed{$_} && $parsed{$_} ne q{} } keys %parsed;
  210         691  
96              
97 42 100       378 return $return{file_name} ? \%return : undef;
98             }
99              
100             sub maybe_get_url_from_parsed_text {
101 0     0 1 0 my $err;
102             try {
103 0     0   0 require_module('Git::Helpers');
104             }
105             catch {
106 0     0   0 $err = $_;
107 0         0 };
108              
109 0 0       0 if ($err) {
110 0         0 warn 'This feature requires Git::Helpers to be installed';
111 0         0 return;
112             }
113              
114 0         0 my $parsed = shift;
115 0 0 0     0 return undef unless $parsed && $parsed->{file_name};
116              
117 0         0 my $url = Git::Helpers::https_remote_url();
118 0 0 0     0 return undef unless $url && $url->can('host');
119 0         0 $parsed->{remote_url} = $url;
120              
121 0         0 my $clone = $url->clone;
122 0         0 my @parts = $clone->path_segments;
123             push(
124             @parts, 'blob', Git::Helpers::current_branch_name(),
125             $parsed->{file_name}
126 0         0 );
127 0         0 $clone->path( join '/', @parts );
128 0 0       0 if ( $parsed->{line_number} ) {
129 0         0 $clone->fragment( 'L' . $parsed->{line_number} );
130             }
131              
132 0         0 $parsed->{remote_file_url} = $clone;
133 0         0 return $clone;
134             }
135              
136             sub to_editor_args {
137 18     18 1 3043 return editor_args_from_parsed_text( parse_text(@_) );
138             }
139              
140             sub editor_args_from_parsed_text {
141 18     18 1 34 my $parsed = shift;
142 18 100       53 return unless $parsed;
143              
144 17         25 my @args;
145              
146             # kate --line 11 --column 2 filename
147 17 100       62 if ( $ENV{EDITOR} eq 'kate' ) {
    100          
148             push @args, '--line', $parsed->{line_number}
149 3 100       9 if $parsed->{line_number};
150              
151             push @args, '--column', $parsed->{column_number}
152 3 100       8 if $parsed->{column_number};
153             }
154              
155             # See https://vi.stackexchange.com/questions/18499/can-i-open-a-file-at-an-arbitrary-line-and-column-via-the-command-line
156             # nvim +'call cursor(11,2)' filename
157             # vi +'call cursor(11,2)' filename
158             # vim +'call cursor(11,2)' filename
159             elsif ( exists $parsed->{column_number} ) {
160 2 100 33     19 if ( $ENV{EDITOR} eq 'nvim'
      66        
161             || $ENV{EDITOR} eq 'vi'
162             || $ENV{EDITOR} eq 'vim' ) {
163             @args = sprintf(
164             '+call cursor(%i,%i)',
165             $parsed->{line_number},
166             $parsed->{column_number},
167 1         11 );
168             }
169              
170             # nano +11,2 filename
171 2 100       8 if ( $ENV{EDITOR} eq 'nano' ) {
172             @args = sprintf(
173             '+%i,%i',
174             $parsed->{line_number},
175             $parsed->{column_number},
176 1         7 );
177             }
178             }
179              
180             else {
181             # emacs +11 filename
182             # nano +11 filename
183             # nvim +11 filename
184             # pico +11 filename
185             # vi +11 filename
186             # vim +11 filename
187 12 100       39 push @args, '+' . $parsed->{line_number} if $parsed->{line_number};
188             }
189              
190 17         131 return ( @args, $parsed->{file_name} );
191             }
192              
193             sub _maybe_extract_line_number {
194 53     53   14903 my $text = shift; # scalar ref
195              
196             # Ansible: ": line 14, column 16,"
197 53 100       196 if ( $$text =~ s{ line (\d+).*, column (\d+).*}{} ) {
198 4         21 return $1, $2;
199             }
200              
201             # Find a line number
202             # lib/Foo/Bar.pm line 222.
203              
204 49 100       160 if ( $$text =~ s{ line (\d+).*}{} ) {
205 7         30 return $1;
206             }
207              
208             # mvn test output
209             # lib/Open/This.pm:[17,3]
210 42 100       136 if ( $$text =~ s{ (\w) : \[ (\d+) , (\d+) \] }{$1}x ) {
211 1         8 return $2, $3;
212             }
213              
214             # ripgrep (rg --vimgrep)
215             # ./lib/Open/This.pm:17:3
216 41 100       179 if ( $$text =~ s{(\w):(\d+):(\d+).*}{$1} ) {
217 5         43 return $2, $3;
218             }
219              
220             # git-grep (don't match on ::)
221             # lib/Open/This.pm:17
222 36 100       138 if ( $$text =~ s{(\w):(\d+)\b}{$1} ) {
223 6         25 return $2;
224             }
225              
226             # Github links: foo/bar.go#L100
227             # Github links: foo/bar.go#L100-L110
228             # In this case, discard everything after the matching line number as well.
229 30 100       99 if ( $$text =~ s{(\w)#L(\d+)\b.*}{$1} ) {
230 5         21 return $2;
231             }
232              
233             # git-grep contextual match
234             # lib/Open/This.pm-17-
235 25 100       102 if ( $$text =~ s{(\w)-(\d+)\-{0,1}\z}{$1} ) {
236 2         26 return $2;
237             }
238              
239 23         61 return undef;
240             }
241              
242             sub _maybe_filter_text {
243 42     42   61 my $text = shift;
244              
245             # Ansible: The error appears to be in '/path/to/file':
246 42 100       136 if ( $$text =~ m{'(.*)'} ) {
247 4         10 my $maybe_file = $1;
248 4 50       97 $$text = $maybe_file if -e $maybe_file;
249             }
250             }
251              
252             sub _maybe_extract_subroutine_name {
253 46     46   5258 my $text = shift; # scalar ref
254              
255 46 100       182 if ( $$text =~ s{::(\w+)\(.*\)}{} ) {
256 9         35 return $1;
257             }
258 37         77 return undef;
259             }
260              
261             sub _maybe_extract_line_number_via_sub_name {
262 23     23   51 my $file_name = shift;
263 23         35 my $sub_name = shift;
264              
265 23 100 100     347 if ( $file_name && $sub_name && open( my $fh, '<', $file_name ) ) {
      66        
266 5         15 my $line_number = 1;
267 5         150 while ( my $line = <$fh> ) {
268 21 100       131 if ( $line =~ m{sub $sub_name\b} ) {
269 5         106 return $line_number;
270             }
271 16         54 ++$line_number;
272             }
273             }
274             }
275              
276             sub _maybe_extract_file_from_url {
277 4     4   7 my $text = shift;
278              
279 4         10 require_module('URI');
280 4         91 my $uri = URI->new($$text);
281              
282 4         10692 my @parts = $uri->path_segments;
283              
284             # Is this a GitHub(ish) URL?
285 4 50       368 if ( $parts[3] eq 'blob' ) {
286 4         18 my $file = path( @parts[ 5 .. scalar @parts - 1 ] );
287              
288 4 50       222 return unless $file->is_file;
289              
290 4         107 $file = $file->stringify;
291              
292 4 50 66     33 if ( $uri->fragment && $uri->fragment =~ m{\A[\d]\z} ) {
293 0         0 $file .= ':' . $uri->fragment;
294             }
295 4 50       72 return $file if $file;
296             }
297             }
298              
299             sub _maybe_find_local_file {
300 9     9   1856 my $text = shift;
301 9         32 my $possible_name = module_notional_filename($text);
302             my @dirs
303             = exists $ENV{OPEN_THIS_LIBS}
304             ? split m{,}, $ENV{OPEN_THIS_LIBS}
305 9 100       471 : ( 'lib', 't/lib' );
306              
307 9         25 for my $dir (@dirs) {
308 17         346 my $path = path( $dir, $possible_name );
309 17 100       695 if ( $path->is_file ) {
310 5         120 return "$path";
311             }
312             }
313 4         83 return undef;
314             }
315              
316             sub _maybe_git_diff_path {
317 19     19   1396 my $file_name = shift;
318              
319 19 100       74 if ( $file_name =~ m|^[ab]/(.+)$| ) {
320 3 100       11 if ( -e path($1) ) {
321 1         67 return $1;
322             }
323             }
324              
325 18         239 return undef;
326             }
327              
328             # search for binaries in path
329              
330             sub _which {
331 20     20   192 my $maybe_file = shift;
332 20 50       47 return unless $maybe_file;
333              
334 20         76 require_module('File::Spec');
335              
336             # we only want binary names here -- no paths
337 20         976 my ( $volume, $dir, $file ) = File::Spec->splitpath($maybe_file);
338 20 100       75 return if $dir;
339              
340 11         159 my @path = File::Spec->path;
341 11         38 for my $path (@path) {
342 71         1939 my $abs = path( $path, $file );
343 71 100       2668 return $abs->stringify if $abs->is_file;
344             }
345              
346 9         191 return;
347             }
348              
349             sub _find_go_files {
350 3     3   884 my $text = shift;
351 3   100     13 my $threshold_init = shift || 5000;
352              
353 3         5 my $file_name;
354 3         10 my $iter = path('.')->iterator( { recurse => 1 } );
355 3         230 my $threshold = $threshold_init;
356 3         11 while ( my $path = $iter->() ) {
357 262 100       30509 next unless $path->is_file; # dirs will never match anything
358 179 100       2735 ( $threshold, $file_name ) = ( 0, "$path" )
359             if $path->basename eq $text;
360 179 100       4766 last if --$threshold == 0;
361             }
362 3 100 66     89 if ( $threshold == 0 && !$file_name ) {
363 1         21 warn "Only $threshold_init files searched recursively";
364             }
365 3         68 return $file_name;
366             }
367              
368             # ABSTRACT: Try to Do the Right Thing when opening files
369             1;
370              
371             __END__