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
By using :# (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
: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 anyone'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 : + 10000 + <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 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. I'll save you the details (or detail them if you want) and go straight to the 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.