Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save yashhere/2c3dc04ca92a92ee9d4460a62fbb2c13 to your computer and use it in GitHub Desktop.
Save yashhere/2c3dc04ca92a92ee9d4460a62fbb2c13 to your computer and use it in GitHub Desktop.
Vim: pushing built-in features beyond their limits

The situation

Searching can be an efficient way to navigate the current buffer.

The first search commands you learn are usually / and ?. These are seriously cool, especially with the incsearch option enabled which lets us keep typing to refine our search pattern. But / and ? really shine when all you want is to jump to something you already have your eyeballs on but they are not fit for every situation:

  • when you want to search something that's not directly there, those two commands can make you loose context very quickly,
  • when you need to be able to compare the matches.

A better way

A better candidate for those situations would be the awesome :g[lobal]/pattern/command but it uses the :p[rint] command by default, which is not that useful:

:g/foo
fqjsfd foo
foo dhqdgqs
    // foo shdjfksgdf
Press ENTER or type command to continue

With :# (or :nu[mber]), we ask Vim to also display the line numbers that we can then use at the prompt:

:g/foo/#
  3 fqjsfd foo
 12 foo dhqdgqs
 13     // foo shdjfksgdf
Press ENTER or type command to continue
:12

to jump to the desired line.

This doesn't look like much but :g/pattern/# is an immensely useful tool that deserves its place in everyone's toolbox. No setup, no dependency, no third-party plugin… it just works!

And will always do.

A better better way

As is, this command requires us to type:

  • :g/ before our pattern,
  • /#<CR> after our pattern,
  • and : followed by 1 to infinite digits, folowed by <CR>.

The pattern and the line number can't be known in advance, of course, but we can certainly simplify the rest.

Starting with :g/pattern/#<CR>, it's relatively easy to come up with a simple mapping:

nnoremap <key> :g//#<Left><Left>

that populates the command-line with a command stub and moves the cursor between the slashes, ready for us to type the pattern and press <CR>:

:g/|/#

Executing our search is now reduced to <key> + pattern + <CR>, which is not bad at all. But what about the prompt? What if we could reduce : + digits + <CR>?

This problem is a bit more complex but it's very doable with a bit of straightforward vimscript:

function! CCR()
    let cmdline = getcmdline()
    if cmdline =~ '\v\C/(#|nu|num|numb|numbe|number)$'
        return "\<CR>:"
    else
        return "\<CR>"
    endif
endfunction
cnoremap <expr> <CR> CCR()

The idea is to automatize the <CR> then : sequence when the command we typed (or ran through a mapping) ends with /# or any abbreviation of /nu[mber].

We are now down to <key> + pattern + <CR> + digits + <CR>. Yes, that's a "core" of only three motherfucking keystrokes for the whole process!

Generalizing

Sure, streamlining that boring (but mighty) :g/foo/#<CR>:17<CR> was quite an achievement but we learned a lot of similar list-like commands in the meantime: :ls, :changes, :ilist and so on. What if we could add an automatic prompt for all of them?

Well, we already have a solid foundation, so let's augment it a bit:

function! CCR()
    let cmdline = getcmdline()
    if cmdline =~ '\v\C/(#|nu|num|numb|numbe|number)$'
        return "\<CR>:"
    elseif cmdline =~ '\v\C^(ls|files|buffers|dli|il|cli|lli|old|changes|ju|marks|undol)'
        return "\<CR>:"
    else
        return "\<CR>"
    endif
endfunction

This seems to work: pressing <CR> after any of those commands executes the command and populates the command-line with a minimalist prompt. Another achievement unloc… Wait! Wait! Wait! Nope. That's acually a super dumb way to treat our "problem"!

Why? Because all of those commands actually have different prompts. Sure # being all about listing lines, the only reasonable prompt is a colon, but :ls lists buffers so the right prompt is :b, :changes lists entries in the change list so the right prompt is :norm! g;, and so on. Don't panic! We only have to add a bunch of conditions to our test. That's all.

Hmm… and figure out the right prompt for each command:

Command Prompt (* marks the desired cursor position)
:#, :nu[mber] :*
:ls, :files, :buffers :b*
:il[ist] :ij[ump] * pattern
:dli[st] :dj[ump] * pattern
:cl[ist] :cc *
:lli[st] :ll *
:old[files] :e[dit] #<*
:changes :norm[al]! *g;
:ju[mps] :norm[al]! *<C-o>
:marks :norm[al]! '*
:undol[ist] :u[ndo] *

Yeah, some of those are a bit unintuitive. That's one more reason for streamlining the whole thing, right?

And now, the glorious (and commented) result:

" make list-like commands more intuitive
function! CCR()
    let cmdline = getcmdline()
    if cmdline =~ '\v\C^(ls|files|buffers)'
        " like :ls but prompts for a buffer command
        return "\<CR>:b"
    elseif cmdline =~ '\v\C/(#|nu|num|numb|numbe|number)$'
        " like :g//# but prompts for a command
        return "\<CR>:"
    elseif cmdline =~ '\v\C^(dli|il)'
        " like :dlist or :ilist but prompts for a count for :djump or :ijump
        return "\<CR>:" . cmdline[0] . "j  " . split(cmdline, " ")[1] . "\<S-Left>\<Left>"
    elseif cmdline =~ '\v\C^(cli|lli)'
        " like :clist or :llist but prompts for an error/location number
        return "\<CR>:sil " . repeat(cmdline[0], 2) . "\<Space>"
    elseif cmdline =~ '\C^old'
        " like :oldfiles but prompts for an old file to edit
        set nomore
        return "\<CR>:sil se more|e #<"
    elseif cmdline =~ '\C^changes'
        " like :changes but prompts for a change to jump to
        set nomore
        return "\<CR>:sil se more|norm! g;\<S-Left>"
    elseif cmdline =~ '\C^ju'
        " like :jumps but prompts for a position to jump to
        set nomore
        return "\<CR>:sil se more|norm! \<C-o>\<S-Left>"
    elseif cmdline =~ '\C^marks'
        " like :marks but prompts for a mark to jump to
        return "\<CR>:norm! `"
    elseif cmdline =~ '\C^undol'
        " like :undolist but prompts for a change to undo
        return "\<CR>:u "
    else
        return "\<CR>"
    endif
endfunction
cnoremap <expr> <CR> CCR()

Basically, that mapping and its associated function don't really change anything fundamental. We still use :ilist or :oldfiles as we used to, but we don't have to type a different prompt for every command anymore. All we did was reducing friction and have a lot of fun in the process.

OK, but we still have only one mapping, for :g//#. Why don't we create mappings for all those commands?

Sure you can go on a hunt for available keys and create mappings for just about every command but that will make a lot to remember for commands you don't use that much anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment