Show Perl subname in vim statusline

I asked on the vim mailing list how to see the name of Perl's current sub/method in the status line and Alan Young, the author of PPIx::IndexLines has a great suggestion which unfortunately relied on PPI. I'm working with very large modules and PPI ground to a halt for me. As a result, I took his suggestion and worked out the following.

First, make sure that your .vimrc has set laststatus=2 in it. That will ensure that you always get a status line, even if you only have one window (i.e., don't have split windows). Then drop the following into your .vim/ftplugin/perl.vim:

if ! exists("g:did_perl_statusline")
    setlocal statusline+=%(\ %{StatusLineIndexLine()}%)
    setlocal statusline+=%=
    setlocal statusline+=%f\ 
    setlocal statusline+=%P
    let g:did_perl_statusline = 1
endif

if has( 'perl' )
perl << EOP
    use strict;
    sub current_sub {
        my $curwin = $main::curwin;
        my $curbuf = $main::curbuf;

        my @document = map { $curbuf->Get($_) } 0 .. $curbuf->Count;
        my ( $line_number, $column  ) = $curwin->Cursor;

        my $sub_name = '(not in sub)';
        for my $i ( reverse ( 1 .. $line_number  -1 ) ) {
            my $line = $document[$i];
            if ( $line =~ /^\s*sub\s+(\w+)\b/ ) {
                $sub_name = $1;
                last;
            }
        }
        VIM::DoCommand "let subName='$line_number: $sub_name'";
    }
EOP

function! StatusLineIndexLine()
  perl current_sub()
  return subName
endfunction
endif

All this does is naïvely read backwards from the current line to get "sub $name" and report $name. It will fail on many common cases. However, it's fast. Very fast. Unlike the PPI solution, I can use this and manually correct any files which don't fit this convention.

It's a quick and nasty hack, but already I'm finding it very useful. Suggestions welcome :)

Note that this requires Perl support. Just ":echo has('perl')" and if it displays '1', you're good to go. Then type "help perl-using" to see what's going on.

Update: I've updated that statusline. There's a space after the '\' and it now shows the column, filename and percent. See ":help statusline" or this blog post for more ideas.

Update2: Changed "set" to "setlocal" so we don't screw with non-Perl buffers.

Update3: If I do an ":e $anotherfile", I lose the new status line. Eliminating the exists("g:did_perl_statusline") seems to fix this.

12 Comments

from the snarky-but-true dept...

Alternatively, use short enough subs that you can always see the sub name when editing the sub (and manually correct any files which don't fit this convention).

Nifty!

Now if we could get petdance to include that snippet, we might see the first 2011 commit here: https://github.com/petdance/vim-perl

In Emacs this is:

M-x which-function-mode

Which you can automatically load with a mode hook. It'll work with any programming mode that supports Imenu. Which is almost all of them.

A bit of bike-shedding, but I like:

  • to see (not in sub) when I'm between subs
  • to see :Attributes for the sub
  my $line = $document[$i];
  last if $line =~ /^}/ and $i != ($line_number-1);
  if ( $line =~ /^\s*sub\s+(\w+\b.*?){?$/ ) {   

Or just rotate the monitor vertically; works ;)

From the bike-shedding department, the lines:

 

  my @document = map { $curbuf->Get($_) } 0 .. $curbuf->Count;
  my ( $line_number, $column  ) = $curwin->Cursor;

 

can become:

 

  my ( $line_number, $column  ) = $curwin->Cursor;
  $line = $curbuf->Get($line_number--)
    while $line_number >=0 && !/^\s* sub \s+ (\w+)/x;

 

.. so you can avoid ->Get-ting all 8k lines when you need "only" 200 ;)

From the bike-shedding department, the lines:

 

  my @document = map { $curbuf->Get($_) } 0 .. $curbuf->Count;
  my ( $line_number, $column  ) = $curwin->Cursor;

 

could become:

 

  my ( $line_number, $column  ) = $curwin->Cursor;
  $line = $curbuf->Get($line_number--)
    while $line_number >=0 && !/^\s* sub \s+ (\w+)/x;

 

.. so you can avoid ->Get-ting all 8k lines when you need "only" 200 ;)

Uh, double comment above, you may remove one.

Corrections after having used this a bit:

b:did_perl_statusline

makes the variable local to the buffer instead of g: which is global

my $line_number = ($curwin->Cursor)[0];

my $line = '';
$line = $curbuf->Get( $line_number )
    while $line_number-- > 0 && $line !~ /^\s*sub\s+(\w+)\b/;

my $sub_name = $1 || '(not in sub)';
VIM::DoCommand("let subName='$sub_name'");

I do believe this is faster than getting all lines for each cursor movement :)

Here's a function that determines the name of the current sub in much the same way, but it's in Vim script, so it works even if you don't have Perl embedded in Vim:


function! PerlCurrentSubName()
    let s:currline = line('.')
    let s:currcol = col('.')
    normal $
    let [s:line, s:column] = searchpos('^\s*sub\s\+\zs\(\w\+\)','bcW')
    if (s:line != 0)
        let s:subname = expand('')
        call cursor(s:currline, s:currcol)
        return s:subname
    else
        return '(not in sub)'
    endif
endfunction

Actually, in this function we need to call cursor() even if there was no match, because "normal $" would insist on always keeping the cursor on the end of the line...


function! PerlCurrentSubName()
    let s:currline = line('.')
    let s:currcol = col('.')
    normal $ 
    let [s:line, s:column] = searchpos('^\s*sub\s\+\zs\(\w\+\)','bcW')
    if (s:line != 0)
        let s:subname = expand('')
    else
        let s:subname = '(not in sub)'
    endif
    call cursor(s:currline, s:currcol)
    return s:subname
endfunction

There may be a better way of doing this without "normal $", and maybe there's a way of getting the result of the subpattern in search() directly without having to jump to the location and expanding cword, but my vim-fu is weak...

I played with your script for about an hour and made an improvement. This will display 'N/A' if you're outside the subroutine. If you have a history of your closing brace indent not matching your sub declaration indent you may want to alter the $indent portion. Have fun!

.vimrc

syntax on
setlocal laststatus=2
setlocal statusline=\ %{HasPaste()}%F%m%r%h\ %w\ \ CWD:\ %r%{CurDir()}%h\ \ \ Position:\ %p%%\ %l/%L,%c
if has("autocmd")
    autocmd BufReadPost * if &syntax == 'perl' | source ~/.vim/perl_current_subroutine | endif
endif



.vim/perl_current_subroutine

if ! exists("b:did_perl_statusline") && &syntax == 'perl'
    setlocal statusline+=%(\ \ \ Subroutine:\ %{StatusLineIndexLine()}%)
    let b:did_perl_statusline = 1
endif

if has('perl')
perl Cursor)[0];
        my $line = $curbuf->Get($line_number);
        my $indent = '';
        if ($line !~ /^(\s*)sub\s+(\w+)\b/) {
            $line = $curbuf->Get($line_number) while ($line_number-- > 0 && $line !~ /^(\s*)sub\s+(\w+)\b/);
            ($indent, $sub_name) = ($1, $2);
        } else {
            $line_number--;
            ($indent, $sub_name) = ($1, $2);
        }

        #if found, (try to) find the end of the subroutine
        if ($sub_name ne 'N/A') {
            my $end = $curbuf->Count();
            $line = $curbuf->Get($line_number);
            if ($line !~ /}\s*$/) {
                $line = $curbuf->Get($line_number) while ($line_number++ Cursor)[0] > $line_number);
        }

        VIM::DoCommand("let current_perl_subroutine_name='$sub_name'");
    }
EOP

    function! StatusLineIndexLine()
        perl current_perl_subroutine()
        return current_perl_subroutine_name
    endfunction
endif

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/