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 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 that : + digits + <CR> part?

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 actually 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 and/or require quite a lot of typing. That's one more reason for streamlining the whole thing, right?

One more thing to consider is how Vim orders items in those lists and how long they can be. The lists generated by :#, :ls, :ilist, :dlist, :clist, :llist, or :marks are relatively short but those generated by :jumps, :oldfiles, or :changes can be 100 lines long and require paging. This can be really cumbersome, especially considering that the most recent items are near the end of the list. For this reason, I chose to temporarily :set nomore in order to jump directly to the end of the list. This is not as clean as I would like it to be but well…

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. Instead, consider this function and this command-line mode mapping as "enablers" that make using those list-like commands a lot easier. If you notice one such command creeping into your workflow, well… map it!

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