Skip to content

Instantly share code, notes, and snippets.

@romainl
Last active November 9, 2024 19:32
Show Gist options
  • Save romainl/f7e2e506dc4d7827004e4994f1be2df6 to your computer and use it in GitHub Desktop.
Save romainl/f7e2e506dc4d7827004e4994f1be2df6 to your computer and use it in GitHub Desktop.
Quickfix alternative to :g/foo/#

Quickfix alternative to :g/foo/#

:help :global is an incredibly cool command.

One thing I like to do with :global is to list lines matching a given pattern in the current file and use that to move around. It looks like this:

:g/let/#
 7         let &path .= 'src/**,public/**,static/**'
 31     unlet b:gqview
 33 nmap <silent> GQ :let b:gqview = winsaveview()<CR>:set opfunc=Format<CR>g@
[...]
:

I like it. I use it all the time.

What if there was a way to persist the result of :[range]g/foo/# and use it to move around?

Well, that's what :[range]Global <pattern> does: it simulates :[range]g/<pattern>/# and populates the location list with the result, allowing us to navigate that list with :help :lnext, :help :lprevious, filter it with :help :Lfilter, operate on each line with :help :ldo, etc.

And :[range]Global! <pattern> does the same but for :[range]g!/<pattern>/#/:[range]v/<pattern>/#.

Neat.

Usage

List all lines with foo:

:[range]Global foo

List all lines without foo:

:[range]Global! foo

My Vim-related gists.

function! Global(pat, bang) range
let operator = a:bang == '!' ? '!~' : '=~'
let padding_top = a:firstline > 1 ? a:firstline - 1 : 0
let bufnr = bufnr()
let qf_title_range = a:firstline == 1 && a:lastline == line('$') ? '%' : a:firstline .. ',' .. a:lastline
let qf_list = getline(a:firstline, a:lastline)
\ ->map({ idx, val -> { 'bufnr': bufnr, 'lnum': idx + 1 + padding_top, 'text': val, 'valid': 1 } })
\ ->filter({ idx, val -> eval("val.text " .. operator .. "'.*' . a:pat . '.*'") })
if qf_list->empty() == 0
call setloclist(win_getid(), [], ' ', { 'title': ':' .. qf_title_range .. 'Global ' .. a:pat, 'items': qf_list })
lwindow
lfirst
endif
endfunction
command! -bang -nargs=1 -range=% Global <line1>,<line2>call Global(<q-args>, expand('<bang>'))
@habamax
Copy link

habamax commented May 3, 2021

too many bangs there:

command! -bang -nargs=1 Global call setloclist(0, [], ' ',
            \ {'title': 'Global ' .. <q-args>,
            \  'efm':   '%f:%l\ %m,%f:%l',
            \  'lines': execute('g<bang>/' .. <q-args> .. '/#')
            \           ->split('\n')
            \           ->map({_, val -> expand("%") .. ":" .. trim(val, 1)})
            \ })

@RaZ0rr-Two
Copy link

RaZ0rr-Two commented Jun 12, 2021

I know this is a bit off-topic but there was a tip on 'where to place custom functions/commands' (or something along those lines), like in which folder in the vim runtimepath. I can't find the gist where it was mentioned unfortunately. So, I am asking here. Sorry :)

@romainl
Copy link
Author

romainl commented Jun 12, 2021

@RaZ0rr-Two

I know this is a bit off-topic but there was a tip on 'where to place custom functions/commands' (or something along those lines), like in which folder in the vim runtimepath. I can't find the gist where it was mentioned unfortunately. So, I am asking here. Sorry :)

If that's a gist of mine you are thinking of, it may be https://github.com/romainl/idiomatic-vimrc.

If you have specific questions, feel free to join us at #vim on libera.chat.

@craigmac
Copy link

@habamax your version isn't working for me until I remove the second argument from the trim() call, like this:

command! -bang -nargs=1 Global call setloclist(0, [], ' ',
            \ {'title': 'Global<bang> ' .. <q-args>,
            \  'efm':   '%f:%l\ %m,%f:%l',
            \  'lines': execute('g<bang>/' .. <q-args> .. '/#')
            \           ->split('\n')
            \           ->map({_, val -> expand("%") .. ":" .. trim(val)})
            \ })

Otherwise you'll get Invalid argument: 1.

@romainl
Copy link
Author

romainl commented Jan 28, 2023

FWIW, this contrived variant of @craigmac's variant of @habamax's variant adds the column number.

command! -bang -nargs=1 Global call setloclist(0, [], ' ',
            \ {'title': 'Global<bang> ' .. <q-args>,
            \  'efm':   '%f:%l:%c\ %m,%f:%l',
            \  'lines': execute('g<bang>/' .. <q-args> .. '/#')
            \           ->split('\n')
            \           ->map({_, val -> expand("%") .. ":" .. trim(val)->substitute('^\d\+','&:' .. trim(val)->substitute('^\d\+ ','','')->charidx(trim(val)->substitute('^\d\+','','')->match(<q-args>)),'')})
            \ })

@romainl
Copy link
Author

romainl commented Jan 26, 2024

FWIW, I made a significant update to this snippet:

  • not a one-liner anymore
  • handles named and unnamed buffers
  • handles arbitrary range
  • sets the quickfix title (inspired by the variations posted in the comments)

@Matt-Deacalion
Copy link

Thank you!

Vim9Script:

# Filters:
#   :Global pattern      - Show lines matching pattern
#   :Global! pattern     - Show lines NOT matching pattern
# Range:
#   :%Global pattern     - Whole file (default)
#   :1,10Global pattern  - Lines 1-10

def Global(line1: number, line2: number, pat: string, bang: bool)
   var lines = getline(line1, line2)
   var matches = []

   for idx in range(len(lines))
       if bang ? lines[idx] !~ pat : lines[idx] =~ pat
           matches->add({
               bufnr: bufnr(),
               lnum: line1 + idx,
               text: lines[idx],
               valid: true,
           })
       endif
   endfor

   if !empty(matches)
       setloclist(win_getid(), matches)
       lwindow
   endif
enddef

command -bang -nargs=1 -range=%
   \ Global Global(<line1>, <line2>, <q-args>, <bang>0)

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