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 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.
As is, this command requires us to type:
:g/before ourpattern,/#<CR>after ourpattern,- 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!
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.