Method Extraction in Vim

I'm hacking on code with some methods which are fairly long (inlined code for performance), but sometimes I have to extract some code out into its own method. Padre uses Devel::Refactor for this, but I didn't want to go down that road as it doesn't use PPI. Thus, I hacked my own using PPIx::EditorTools. It's not great, but long-term, I think it's a more robust solution.

In vim, I have the following binding for Perl:

vnoremap <leader>sub :!~/bin/extract_sub <cr>

Thus, when I select code in vim with shift-v, I can type ",sub" and that passes the select code via STDIN to this filter:

#!/usr/bin/env perl

use strict;
use warnings;
use PPI;
use PPIx::EditorTools 0.11;

my $code = do { local $/; <STDIN> };
chomp($code);
my $ppi = PPI::Document->new( \$code );

my %is_found =
  map { $_ => 1 }
  map { keys %$_ }
  values %{ PPIx::EditorTools::get_all_variable_declarations($ppi) };

my $vars = $ppi->find(
    sub {
        my ( $self, $thingy ) = @_;
        no warnings 'uninitialized';
        return $thingy->isa('PPI::Token::Symbol')
          && not $is_found{ $thingy->content }++;
    }
);

my $subname = $ENV{SUBNAME} || 'XXX';

my $unroll_at_underscore = '';
if ($vars) {
    $unroll_at_underscore =
      "    my ( " . ( join ', ' => map { $_->content } @$vars ) . " ) = \@_;\n";
}
print <<"END";
sub $subname {
$unroll_at_underscore
$code
}
END

And the "print" at the end writes the code back to the selected area, expanding it as needed. So let's say we have the following method:

sub process_doc {
    my ( $self, %args ) = @_;

    $self->ppi( $args{ppi} ) if defined $args{ppi};
    return 1 if $self->ppi && $self->ppi->isa('PPI::Document');

    # TODO: inefficient to pass around full code/ppi
    $self->code( $args{code} ) if $args{code};
    my $code = $self->code;
    $self->ppi( PPI::Document->new( \$code )  );
    return 1 if $self->ppi && $self->ppi->isa('PPI::Document');

    croak "arguments ppi or code required";
    return;
}

If you want the "# TODO" line and the following four lines extracted into a method, you select them and run the filter. That results in the following:

sub process_doc {
    my ( $self, %args ) = @_;

    $self->ppi( $args{ppi} ) if defined $args{ppi};
    return 1 if $self->ppi && $self->ppi->isa('PPI::Document');

sub XXX {
    my ( $self, $args ) = @_;

    # TODO: inefficient to pass around full code/ppi
    $self->code( $args{code} ) if $args{code};
    my $code = $self->code;
    $self->ppi( PPI::Document->new( \$code )  );
    return 1 if $self->ppi && $self->ppi->isa('PPI::Document');
}

    croak "arguments ppi or code required";
    return;
}

That's not legal Perl and it seems trivial, but when you've selected a hundred lines of code, it's very handy. It clearly needs a lot more work, but it's already saving me a lot of time and frustration.

6 Comments

Cute.

Did you ever take a look at App::EditorTools and/or PPIx::EditorTools?

Oh. Indeed. As always, reading the source helps quite a bit.

I saw a script somewhere once that did this without PPI, and more correctly. It just piped the code to perl -c -Mstrict=vars and parsed the stricture failure error messages from perl to figure out which variables need to be declared.

I've been using a script by Jesse Vincent, ever since Piers Cawley mentioned it.

@Aristotle, I see you commented on that post. Is this the script you mean?

Yes! Good grief, I scoured heaven and hell to find it and failed. Thank you!

About Ovid

user-pic Freelance Perl/Testing/Agile consultant and trainer. See http://www.allaroundtheworld.fr/ for our services. If you have a problem with Perl, we will solve it for you. And don't forget to buy my book! http://www.amazon.com/Beginning-Perl-Curtis-Poe/dp/1118013840/