Search code examples
regexvimvivimgrep

VIM - Hotkey for VIMGREP on Visual Selection in current buffer


How would I go about setting up a hotkey (eg: CTRL+g) to perform a VIMGREP operation on the current visual selection in the current buffer? My intent is to show a line-numbered list in the "quickfix" window of all matching search results.

Right now, if I want to get a list of results for a regex search, I could do a command-mode query like so:

:vimgrep /foo/ %

However, there are two problems with this:

  1. I don't want to have to type out the entire query. I could always do a visual selection, then use CTRL+r, CTRL+w, to paste the current visual selection into the command buffer, but I'd like something simpler than this.
  2. The above approach requires that the current buffer is already saved to a file. I'd like to be able to work on a temporary buffer I've pasted into VIM rather than having to save a file buffer each time I want to do this.

Thank you.


Solution

  • A low-level solution

    Try [I and the :ilist command:

    [I                 " lists every occurrence of the word under the cursor
                       " in the current buffer (and includes)
    
    :ilist /foo<CR>    " lists every occurrence of foo in the current buffer 
                       " (and includes)
    

    Press : followed by a line number and <CR> to jump to that line.

    You can use them on the visual selection with a simple mapping:

    xnoremap <key> "vy:<C-u>ilist /<C-r>v<CR>:
    

    You'll probably need to sanitize the register upon insertion, though.

    See :help :ilist.

    Another even lower-level solution

    Since we are at it, let's dig even deeper and find the amazingly simple and elegant:

    :g/foo/#
    

    that you could use in the same way as :ilist above:

    xnoremap <key> "vy:<C-u>g/<C-r>v/#<CR>:
    

    Limitations

    The solutions above don't use the quickfix window, obviously, but they allow you to:

    • see their result as a list,
    • use line numbers to actually get to where you want.

    They have limitations, though:

    • the list is not cached so you must perform the search again if you want to get to a different occurrence,
    • the list is not transient like the quickfix list so you can't use navigation commands like :cnext or :clast to move around the result.

    A higher-level solution

    If those limitations are a showstopper, the function below, adapted from justinmk's answer in this /r/vim thread, gives you an almost complete solution:

    • press [I in normal mode to search for the word under the cursor in the whole buffer,
    • press ]I in normal mode to search for the word under the cursor after the current line,
    • press [I in visual mode to search for the selected text in the whole buffer,
    • press ]I in visual mode to search for the selected text after the current line.

    The function below uses the quickfix list/window when the buffer is associated to a file and falls back to the regular behavior of [I and ]I otherwise. It could probably be modified to be used as part of an :Ilist command.

    " Show ]I and [I results in the quickfix window.
    " See :help include-search.
    function! Ilist_qf(selection, start_at_cursor)
    
        " there's a file associated with this buffer
        if len(expand('%')) > 0
    
            " we are working with visually selected text
            if a:selection
    
                " we build a clean search pattern from the visual selection
                let old_reg = @v
                normal! gv"vy
                let search_pattern = substitute(escape(@v, '\/.*$^~[]'), '\\n', '\\n', 'g')
                let @v = old_reg
    
                " and we redirect the output of our command for later use
                redir => output
                    silent! execute (a:start_at_cursor ? '+,$' : '') . 'ilist /' . search_pattern
                redir END
    
            " we are working with the word under the cursor
            else
    
                " we redirect the output of our command for later use
                redir => output
                    silent! execute 'normal! ' . (a:start_at_cursor ? ']' : '[') . "I"
                redir END
            endif
            let lines = split(output, '\n')
    
            " better safe than sorry
            if lines[0] =~ '^Error detected'
                echomsg 'Could not find "' . (a:selection ? search_pattern : expand("<cword>")) . '".'
                return
            endif
    
            " we retrieve the filename
            let [filename, line_info] = [lines[0], lines[1:-1]]
    
            " we turn the :ilist output into a quickfix dictionary
            let qf_entries = map(line_info, "{
                        \ 'filename': filename,
                        \ 'lnum': split(v:val)[1],
                        \ 'text': getline(split(v:val)[1])
                        \ }")
            call setqflist(qf_entries)
    
            " and we finally open the quickfix window if there's something to show
            cwindow
    
        " there's no file associated with this buffer
        else
    
            " we are working with visually selected text
            if a:selection
    
                " we build a clean search pattern from the visual selection
                let old_reg = @v
                normal! gv"vy
                let search_pattern = substitute(escape(@v, '\/.*$^~[]'), '\\n', '\\n', 'g')
                let @v = old_reg
    
                " and we try to perform the search
                try
                    execute (a:start_at_cursor ? '+,$' : '') . 'ilist /' .  search_pattern . '<CR>:'
                catch
                    echomsg 'Could not find "' . search_pattern . '".'
                    return
                endtry
    
            " we are working with the word under the cursor
            else
    
                " we try to perform the search
                try
                    execute 'normal! ' . (a:start_at_cursor ? ']' : '[') . "I"
                catch
                    echomsg 'Could not find "' . expand("<cword>") . '".'
                    return
                endtry
            endif
        endif
    endfunction
    
    nnoremap <silent> [I :call Ilist_qf(0, 0)<CR>
    nnoremap <silent> ]I :call Ilist_qf(0, 1)<CR>
    xnoremap <silent> [I :<C-u>call Ilist_qf(1, 0)<CR>
    xnoremap <silent> ]I :<C-u>call Ilist_qf(1, 1)<CR>
    

    NB: <C-r><C-w> inserts the word under the cursor, not the visual selection for which there's unfortunately no such shortcut. We have no choice but to yank.