Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 116 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save romainl/047aca21e338df7ccf771f96858edb86 to your computer and use it in GitHub Desktop.
Save romainl/047aca21e338df7ccf771f96858edb86 to your computer and use it in GitHub Desktop.
Vim: pushing built-in features beyond their limits

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 we learn are usually / and ?. These are seriously cool, especially with the incsearch option enabled which lets us keep typing to refine our search pattern. / and ? really shine when all we want is to jump to something we already have our eyeballs on but they are not fit for every situation:

  • when we want to search something that's not directly there, those two commands can make us lose context very quickly,
  • when we 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<CR>
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:

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

that we can use at the prompt:

:12<CR>

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:

  1. :g/,
  2. our pattern,
  3. /#<CR>,
  4. : followed by 1 to infinite digits,
  5. <CR>.

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

Steps 1 and 3

This is the easiest (and thus most boring) part. 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?

Step 4

This problem is a bit more complex (and thus a lot more interesting) but it's very doable with a bit of straightforward vimscript:

function! CCR()
    " grab the current command-line
    let cmdline = getcmdline()
    
    " does it end with '#' or 'number' or one of its abbreviations?
    if cmdline =~ '\v\C/(#|nu|num|numb|numbe|number)$'
        " press '<CR>' then ':' to enter command-line mode
        return "\<CR>:"
    else
        " press '<CR>'
        return "\<CR>"
    endif
endfunction

" map '<CR>' in command-line mode to execute the function above
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 undoubtedly learned a lot of similar list-like commands in the meantime: :ls, :changes, :ilist and so on. Creating a common mapping for all those commands is obviously out of question but… 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 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 would be :b, :changes lists entries in the change list so the right prompt would be :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 prompts 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 the list 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 (and remember) 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//#. What about the other commands?

Sure we could go on a hunt for available keys and create mappings for just about every list-like command but that will make a lot to remember for commands we don't use that much anyway. Instead, let's consider this function and this command-line mode mapping as "enablers" that make using those list-like commands a lot easier.

If we notice one such command creeping into our workflow, well… we will map it!


My Vim-related gists.

@gpanders
Copy link

gpanders commented Oct 16, 2018

UPDATE: I feel dumb. The answer was obvious, I simply had to change nnoremap to simply nmap to expand the <CR>. I will leave my comment here for posterity in case anyone happens to have the same question.


How would one go about using this when combined with another mapping? For example, I would like to use the following normal mode shortcut for :ls:

nnoremap <leader>l :ls<CR>

However, since this gist uses cnoremap that <CR> is not expanded to use this function. I have tried a few different things but I haven't been able to get it to work.

EDIT: I was able to use nnoremap <leader>l :ls<CR>:b which does the same thing for the :ls case without using the CCR() function; however, if there is a more general way to do this that would work with any given command I'd still like to know it.

Things I have tried:

nnoremap <leader>l :ls<C-R>=<sid>CCR()<CR>
nnoremap <expr> <leader>l ":ls".<sid>CCR()
nnoremap <leader>l :ls<sid>CCR()<CR>

@zekzekus
Copy link

zekzekus commented Jan 1, 2019

I just added the registers command to the list. Prompt pastes the selected register content. Thanks for this great idea!

elseif cmdline =~ '\C^reg'
  return "\<CR>:norm! \"p\<Left>"

@g0xA52A2A
Copy link

g0xA52A2A commented Apr 6, 2019

I loved this gist when I first read it and I still love the ethos and enhancement it brings to workflows.

But now I'm finally getting around to integrating this into my setup I'm less keen on the implementation. Namely I don't like the use of regular expressions to match commands.

Instead we could pull the last line from the command history with histget() and take the first expansion of what looks to be the first command* with getcompletion(). This gives us the proper command name which we can match against literally.

Also rather than always having to return a carriage return and whatever else in an expression I've opted to use an autocmd. I feel this is cleaner, obviously that's just personal taste.

Here's what I have. This also fixes what could be considered a minor bug in your dlist and ilist mapping. The pattern may have spaces so rather than increment the cursor backwards move it to the start then increment it forwards to the correct destination.

* This of course doesn't work with things like :g/foo/# and to make that work would require a lot more logic. Instead I take the easy way out and just do nnoremap <key> :global //#<Left><Left> instead.

@josefson
Copy link

How would i go about mapping something like ,m :marks<CR> to trigger this function without me needing to type :marks in the command-line-mode?

@romainl
Copy link
Author

romainl commented May 25, 2020

@josefson, you only need a simple recursive mapping for that:

nmap ,m :marks<CR>

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