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   6547 use strict;
  9         66  
  9         265  
2 9     9   43 use warnings;
  9         17  
  9         794  
3             package Open::This;
4              
5             our $VERSION = '0.000032';
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         53 use Module::Runtime qw(
16             is_module_name
17             module_notional_filename
18             require_module
19 9     9   4743 );
  9         16471  
20 9     9   4741 use Module::Util ();
  9         28520  
  9         336  
21 9     9   9005 use Path::Tiny qw( path );
  9         155167  
  9         574  
22 9     9   4722 use Try::Tiny qw( catch try );
  9         19339  
  9         566  
23 9     9   5111 use URI ();
  9         41864  
  9         18568  
24              
25             ## no critic (Subroutines::ProhibitExplicitReturnUndef)
26              
27             sub parse_text {
28 42     42 1 22077 my $text = join q{ }, @_;
29              
30 42 50       116 if ($text) {
31 42         354 $text =~ s/^\s+|\s+$//g;
32             }
33              
34             # Don't fail on a trailing colon which was accidentally pasted.
35 42 50       98 if ($text) {
36 42         99 $text =~ s{:\z}{};
37             }
38              
39 42 50       135 return undef if !$text;
40 42         144 my %parsed = ( original_text => $text );
41              
42 42         112 my ( $line, $col ) = _maybe_extract_line_number( \$text );
43 42         106 $parsed{line_number} = $line;
44 42 100       99 $parsed{column_number} = $col if $col;
45 42         102 $parsed{sub_name} = _maybe_extract_subroutine_name( \$text );
46              
47 42         116 _maybe_filter_text( \$text );
48              
49             # Is this an actual file.
50 42 100       134 if ( -e path($text) ) {
    100          
    100          
51 23         1575 $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         8 my $file_name = _find_go_files($text);
58 2 100       8 $parsed{file_name} = $file_name if $file_name;
59             }
60              
61 42 100       205 if ( !exists $parsed{file_name} ) {
62 17 100       44 if ( my $bin = _which($text) ) {
63 1         28 $parsed{file_name} = $bin;
64 1         3 $text = $bin;
65             }
66             }
67              
68 42         127 $parsed{is_module_name} = is_module_name($text);
69              
70 42 100 100     831 if ( !$parsed{file_name} && $text =~ m{\Ahttps?://}i ) {
71 4         9 $parsed{file_name} = _maybe_extract_file_from_url( \$text );
72             }
73              
74 42 100 100     149 if ( !$parsed{file_name} && $parsed{is_module_name} ) {
75 7         25 $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     140 if ( !$parsed{file_name} && $parsed{is_module_name} ) {
81 4         20 my $found = Module::Util::find_installed($text);
82 4 100       1869 if ($found) {
83 2         7 $parsed{file_name} = $found;
84             }
85             }
86              
87 42 100       100 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         78 );
92             }
93              
94 118         293 my %return = map { $_ => $parsed{$_} }
95 42 100       151 grep { defined $parsed{$_} && $parsed{$_} ne q{} } keys %parsed;
  210         653  
96              
97 42 100       351 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 3003 return editor_args_from_parsed_text( parse_text(@_) );
138             }
139              
140             sub editor_args_from_parsed_text {
141 18     18 1 43 my $parsed = shift;
142 18 100       54 return unless $parsed;
143              
144 17         30 my @args;
145              
146             # kate --line 11 --column 2 filename
147 17 100       65 if ( $ENV{EDITOR} eq 'kate' ) {
    100          
148             push @args, '--line', $parsed->{line_number}
149 3 100       11 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     17 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         8 );
168             }
169              
170             # nano +11,2 filename
171 2 100       10 if ( $ENV{EDITOR} eq 'nano' ) {
172             @args = sprintf(
173             '+%i,%i',
174             $parsed->{line_number},
175             $parsed->{column_number},
176 1         11 );
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         159 return ( @args, $parsed->{file_name} );
191             }
192              
193             sub _maybe_extract_line_number {
194 53     53   14256 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         22 return $1, $2;
199             }
200              
201             # Find a line number
202             # lib/Foo/Bar.pm line 222.
203              
204 49 100       153 if ( $$text =~ s{ line (\d+).*}{} ) {
205 7         31 return $1;
206             }
207              
208             # mvn test output
209             # lib/Open/This.pm:[17,3]
210 42 100       137 if ( $$text =~ s{ (\w) : \[ (\d+) , (\d+) \] }{$1}x ) {
211 1         5 return $2, $3;
212             }
213              
214             # ripgrep (rg --vimgrep)
215             # ./lib/Open/This.pm:17:3
216 41 100       163 if ( $$text =~ s{(\w):(\d+):(\d+).*}{$1} ) {
217 5         38 return $2, $3;
218             }
219              
220             # git-grep (don't match on ::)
221             # lib/Open/This.pm:17
222 36 100       139 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       96 if ( $$text =~ s{(\w)#L(\d+)\b.*}{$1} ) {
230 5         20 return $2;
231             }
232              
233             # git-grep contextual match
234             # lib/Open/This.pm-17-
235 25 100       94 if ( $$text =~ s{(\w)-(\d+)\-{0,1}\z}{$1} ) {
236 2         9 return $2;
237             }
238              
239 23         60 return undef;
240             }
241              
242             sub _maybe_filter_text {
243 42     42   57 my $text = shift;
244              
245             # Ansible: The error appears to be in '/path/to/file':
246 42 100       138 if ( $$text =~ m{'(.*)'} ) {
247 4         10 my $maybe_file = $1;
248 4 50       89 $$text = $maybe_file if -e $maybe_file;
249             }
250             }
251              
252             sub _maybe_extract_subroutine_name {
253 46     46   5068 my $text = shift; # scalar ref
254              
255 46 100       164 if ( $$text =~ s{::(\w+)\(.*\)}{} ) {
256 9         36 return $1;
257             }
258 37         78 return undef;
259             }
260              
261             sub _maybe_extract_line_number_via_sub_name {
262 23     23   60 my $file_name = shift;
263 23         36 my $sub_name = shift;
264              
265 23 100 100     335 if ( $file_name && $sub_name && open( my $fh, '<', $file_name ) ) {
      66        
266 5         15 my $line_number = 1;
267 5         142 while ( my $line = <$fh> ) {
268 21 100       137 if ( $line =~ m{sub $sub_name\b} ) {
269 5         103 return $line_number;
270             }
271 16         52 ++$line_number;
272             }
273             }
274             }
275              
276             sub _maybe_extract_file_from_url {
277 4     4   8 my $text = shift;
278              
279 4         10 require_module('URI');
280 4         120 my $uri = URI->new($$text);
281              
282 4         8131 my @parts = $uri->path_segments;
283              
284             # Is this a GitHub(ish) URL?
285 4 50       437 if ( $parts[3] eq 'blob' ) {
286 4         20 my $file = path( @parts[ 5 .. scalar @parts - 1 ] );
287              
288 4 50       170 return unless $file->is_file;
289              
290 4         103 $file = $file->stringify;
291              
292 4 50 66     32 if ( $uri->fragment && $uri->fragment =~ m{\A[\d]\z} ) {
293 0         0 $file .= ':' . $uri->fragment;
294             }
295 4 50       70 return $file if $file;
296             }
297             }
298              
299             sub _maybe_find_local_file {
300 9     9   1940 my $text = shift;
301 9         34 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       247 : ( 'lib', 't/lib' );
306              
307 9         25 for my $dir (@dirs) {
308 17         330 my $path = path( $dir, $possible_name );
309 17 100       676 if ( $path->is_file ) {
310 5         127 return "$path";
311             }
312             }
313 4         85 return undef;
314             }
315              
316             sub _maybe_git_diff_path {
317 19     19   1414 my $file_name = shift;
318              
319 19 100       76 if ( $file_name =~ m|^[ab]/(.+)$| ) {
320 3 100       10 if ( -e path($1) ) {
321 1         62 return $1;
322             }
323             }
324              
325 18         202 return undef;
326             }
327              
328             # search for binaries in path
329              
330             sub _which {
331 20     20   168 my $maybe_file = shift;
332 20 50       44 return unless $maybe_file;
333              
334 20         65 require_module('File::Spec');
335              
336             # we only want binary names here -- no paths
337 20         876 my ( $volume, $dir, $file ) = File::Spec->splitpath($maybe_file);
338 20 100       70 return if $dir;
339              
340 11         162 my @path = File::Spec->path;
341 11         45 for my $path (@path) {
342 71         1854 my $abs = path( $path, $file );
343 71 100       2626 return $abs->stringify if $abs->is_file;
344             }
345              
346 9         185 return;
347             }
348              
349             sub _find_go_files {
350 3     3   718 my $text = shift;
351 3   100     11 my $threshold_init = shift || 5000;
352              
353 3         5 my $file_name;
354 3         10 my $iter = path('.')->iterator( { recurse => 1 } );
355 3         192 my $threshold = $threshold_init;
356 3         11 while ( my $path = $iter->() ) {
357 262 100       29334 next unless $path->is_file; # dirs will never match anything
358 179 100       2653 ( $threshold, $file_name ) = ( 0, "$path" )
359             if $path->basename eq $text;
360 179 100       4714 last if --$threshold == 0;
361             }
362 3 100 66     68 if ( $threshold == 0 && !$file_name ) {
363 1         24 warn "Only $threshold_init files searched recursively";
364             }
365 3         52 return $file_name;
366             }
367              
368             # ABSTRACT: Try to Do the Right Thing when opening files
369             1;
370              
371             __END__