Vim's useful lists

A good understanding of Vim’s various lists is a massive productivity boost — it’s taken me many years of Vim use to truly appreciate this.

This post summarises some of Vim’s lists, detailing their purpose and how to make the most of them.


Quickfix list

A global list of locations populated via one of these commands:

  • :vim[grep] — search using Vim’s native functionality.
  • :gr[ep] — search via an external program specified by the grepprg setting.
  • :helpgr[ep] — search help text files.
  • :mak[e] — call the program specified by the makeprg setting (which defaults to make).
  • :cex[pr] {expression} — use {expression} to populate to list. This can be used to clear the quickfix list via :cex [].

Quickfix list commands start :c — here’s some useful ones:

  • :cw[indow] — Open the quickfix window if it’s non-empty.
  • :cn[ext], :cp[rev] — Jump to next/previous error.
  • :cnf[ile], :cpf[ile] — Jump to first error in the next/previous file.
  • :cab[ove] :cbel[ow] — Jump to the error above/below the current line.

Vim remembers the last ten quickfix lists; you can navigate between them with:

  • :col[der], :cnew[er] — Go to the older/newer quickfix list.
  • :chi[story] — Show the list of quickfix lists.

This is powerful: it allows building a tree of searches, which is a useful debugging pattern when exploring a codebase.

More at :help quickfix.

Tip: Use mappings for faster browsing

Since you’ll use them a lot, define convenient mappings for :cnext and :cprev. This can be achieved with tpope/vim-unimpaired, which provides a slew of useful mapping pairs starting with [ or ]. E.g.

  • [q, ]q:cprevious, :cnext
  • [Q, ]Q:cfirst, :clast
  • [<C-Q>, ]<C-Q>:cpfile, :cnfile

There’s a pair of tpope/vim-unimpaired mappings for each list described in this post.

Tip: Define a mapping to :grep for the word under the cursor

I also recommend defining a mapping that runs :grep on the word under the cursor. This makes it trivial to answer the question “where else is this function used?”

I use:

nnoremap gw :grep <cword> . <cr>

although this does clobber a built-in formatting operator.

Tip: Custom grepprg programs

Use ripgrep as your default search program:

if executable('rg')
    set grepprg=rg\ --vimgrep
    set grepformat=%f:%l:%c:%m

Keep your ripgrep configuration in a global ~/.ripgreprc file so :grep behaves the same as rg at the command-line. Recommended settings:

# ~/.ripgreprc

Tip: Custom makeprg programs

The default makeprg is make but any command that prints locations to STDOUT can be used — most static analysis tools are viable candidates.

For instance, I occasionally use it for Bandit security analysis via:

set makeprg=bandit\ -r\ -f\ custom

or to work though all Python linting errors in a new project:

set makeprg=flake8

Tip: Operating on the error list

A powerful editing technique is to apply an operation or macro to each location or file in the quickfix list. This is done with :cdo or :cfdo respectively. Here are some examples where we populate the quickfix list using :grep (using ripgrep options) then perform a batch edit using :cdo or :cfdo.

Replace “foo” with “bar” in all Python files containing the string “baz”:

:grep baz -t py
:cfdo %s/foo/bar/g | update

Delete lines containing “foo” from all files called

:grep foo -g
:cfdo g/foo/d | update

Run macro q on all lines containing “foo”:

:grep foo
:cdo normal @q

I sometimes write a custom function to run on each quickfix location. This is a useful way of performing complex batch changes that are too complex for a single Ex command or macro:

function! FixQuickfixEntry()
    " Example the line of the error to determine what fix is required.
    let line = getline('.')
    if line =~ 'as e:$'
        " Handle scenario of unused exception variable
        s/ as e:/:/
    elseif line =~ '^\s\+\w\+ = factory'
        " Handle scenario of unused test factory variable
        normal _
        normal 2dw
        echom "Unable to fix"

then use:

:cdo call FixQuickfixEntry()

This is good technique for quickly performing sophisticated refactoring across a whole codebase.

Tip: Populate the quickfix list in a subshell

When running :grep or :make, Vim executes the program in Vim’s parent shell. This suspends Vim while results are printed then requires you to hit enter to return.

There’s a brilliant gist from Romain Lafourcade that shows how to avoid this woes by using :cgetexpr (or :cexpr) to populate the quickfix list in a subshell. There’s a few variants you can use depending on whether you regularly search for multi-word queries. I do, so I use this version:

function! Grep(...)
    let cmd = join([&grepprg] + [join(a:000, ' ')], ' ')
    return system(cmd)

command! -nargs=+ -complete=file_in_path Grep cexpr Grep(<f-args>)

To complement this, you can use a command-line abbreviation to automatically switch :grep to :Grep:

cnoreabbrev <expr> grep (getcmdtype() ==# ':' && getcmdline() ==# 'grep') ? 'Grep' : 'grep'

and an autocommand to open the quickfix window when there are results:

augroup quickfix
 autocmd QuickFixCmdPost cexpr cwindow
augroup END

This is life changing.

Location list

A window-local equivalent of the quickfix list — a similar set of commands exists but now prefixed :l (e.g. :lwindow to open the location list if it has entries).

Personally, I don’t use it much directly but it’s good to be aware of. Popular plugins like Ale and Syntastic populate the location list.

More at :help location-list.

Jump list

A window-local list of the last one hundred cursor positions:

  • CTRL-O, CTRL-I — Go to previous/next cursor position in jump list.
  • :jumps — View the jump list.
  • :clearjumps — Clear the jump list.

The following commands as considered “jumps” and will append an entry to the jump list:

  • (, ) — Jump backward/forward a sentence;
  • {, } — Jump backward/forward a paragraph;
  • /, ? — Search forward/backward in current buffer;
  • n, N — Repeat the last / or ? jump in conventional/opposite direction;
  • :s — Substitution;
  • G — Go to line;
  • % — Jump to matching parenthesis/bracket/brace/…
  • ',` — Jump to a mark in the current buffer;
  • H, M, L — Jump to the top/middle/bottom of the current window (adjusting for scrolloff offset).
  • :tag, ^] — Jump to tag definition;
  • [[, ]] — Move one “section” backward/forward, or to the previous { in the first column. Note “sections” in Vim come from Nroff files and are generally not that useful.

Further, commands that start editing files like :edit append to the jumplist.

When you split a window, its jump list is duplicated and jump lists are saved between sessions (in the viminfo file) if the 'viminfo' option includes a ' character.

More at:

Tip: Use jump commands to navigate

The motions j, k, CTRL-U, CTRL-D are not counted as jumps and so don’t create an entry in the jump list. Hence it’s preferable to navigate within a buffer using the above jump commands as this builds a history that you can walk back through (see this Reddit comment).

Change list

A buffer-local list of the last one hundred undo-able changes:

  • g;, g, — Jump to the previous/next change. These can be prefixed with a count to determine which change to jump back/forward to.

  • :changes — View the change list. The output enumerates each change making it easy to determine the count value to use with g; or g,.

Like the jumplist, the changelist is persisted for each file if your viminfo setting contains a '.

More at:

Tip: Jump to the previous location where insert mode was used

Not strictly to do with the change list but the gi command is similar to g;. It will enter insert mode in the current buffer in the last insert mode location. If you made a change when last in insert mode, this will be equivalent to g; but will also put you into insert mode. The same effect can be achieved with marks via '^.

Buffer list

A global, append-only list of buffers (including cursor location and state) for the current editing session. Each opened file will be appended to the list which can be viewed with any of :buffers, :files or most succinctly :ls.

There are many ways to navigate the buffer list, including:

  • :b[uffer] {number} — Jump to buffer by number.
  • :bp[rev], :bn[ext] — Jump to previous/next buffer (will wrap at end of list).
  • :bf[irst], :br[ewind] — Jump to first/last buffer.

or from tpope/vim-unimpaired:

  • [b, ]b — Jump to previous/next buffer.
  • [B, ]B — Jump to first/last buffer.

Some other useful buffer list commands to know:

  • :ba[ll] — Rearrange windows to show a window for each buffer in the buffer list.
  • :bufdo {cmd} — Execute {cmd} in each buffer in the list. For example, apply macro q to each buffer with :bufdo normal @q.

More at:

Tip: Select buffers with FZF

I recommend the :Buffers command from the excellent junegunn/fzf.vim to jump to a buffer using fuzzy matching. For easy access I map :Buffers to ,b and :Files to ,f.

Argument list

The buffer list tends to become cluttered over time, making it awkward to use :bufdo. This is where the argument list comes in — it allows you to curate a list of files to act on with the equivalent :argdo.

The name is misleading: the argument list is not simply the list of files that Vim was opened with. It’s a global, mutable list that can be edited within your Vim session to select exactly the files that you want to operate on.

There are several ways of populating the argument list, listed here in order of increasing usefulness:

  1. Manually add and remove files from the argument list with :arga[dd] and :argd[elete].

  2. Pass files as arguments when opening Vim:

    vim file1 file2

    or using a file finder like fd:

    vim $(fd -e py)  # open files with .py extension
  3. Pipe a list of files to xargs to provide an initial argument list:

    fd -e py | xargs -o vim

    The -o option for xargs re-opens stdin for the Vim process — without this, Vim can break your terminal.

  4. Within a Vim session, use the :args command:

    :args file1 file2

    This approach is powerful as not only does wildcard expansion work:

    :args *.c

    but so does shelling out:

    :args `fd -e py`

    fd is a particularly good tool to use to populate the argument list.

You can check the contents of the argument list with :ar[gs] and traverse with:

  • :p[revious], :n[ext] — Edit the previous/next file in the argument list.
  • :fir[st], :la[st] — Edit the first/last file in the argument list.

Or via tpope/vim-unimpaired:

  • [a, ]a:previous, :next
  • [A, ]A:first, :last

It’s now possible to apply batch operations on each file in the argument list using :argdo {cmd}. An example workflow is:

  1. Select all HTML files beneath current directory:

    :args `fd -e html`
  2. Run a search-and-replace-then-save operation on each file:

    :argdo %s/foo/bar/g | update

More at:

Tag match list

A “tag” is an identifier — like a function, class or variable name — that appears in a “tags file” generated by an external program like ctags. The tags file is effectively an index mapping identifiers to locations in your codebase.

Use the tags setting to tell Vim where to find tag files. A common value is:

set tags=./tags;,tags

which tells Vim to look for tags file in the directory of the current file, in the working directory and in every parent directory, recursively.

You can check which tag files have been loaded with:

:echo tagfiles()

You can jump to a tag definition with:

  • CTRL-] — Look-up the keyword under the cursor. Useful for jumping to a function’s definition.
  • :ta[g] {query} — Find the tag(s) matching {query}. This command also supports command-line completion and regular expression queries like :tag /foobar/.

If you want to open the tag in a split or the preview window, there are analogues of the above commands starting :s and :p.

If there are multiple tag matches, CTRL-] and :tag will jump to the first one. The “tag match list” can be opened with :ts[elect] and traversed via :tf[irst], :tn[ext], :tp[rev], :tl[ast].

As ever, there are useful bindings from tpope/vim-unimpaired:

  • [t, ]t:tprevious, :tnext
  • [T, ]T:tfirst, :tlast

If you want to show the tag match list when there are multiple matches but jump straight to the tag if a single match, use :tj[ump] or g CTRL-].

Vim stores the twenty most recent tag match lists in a stack which you can view with :tags. This allows you to descend into a call graph by repeatedly jumping to function definitions then popping the stack to return to the previous location with CTRL-T or :po[p]. This is analogous to the nested quickfix lists we saw earlier and is a powerful code exploration technique.

More at:

Tip: Use Universal Ctags

Universal Ctags is the successor to Exuberant Ctags; it’s actively maintained and supports a wider array of languages. It’s worth installing to build tags files for your projects.

Tip: Use :tjump as your default “jump to tag” command

If it’s common for your codebase to have multiple matches for identifiers (which is true of Python), consider using :tjump (g CTRL-]) instead of CTRL-] as your default “jump to tag” command".

I swap the meanings of CTRL-] and g CTRL-]:

nnoremap <c-]> g<c-]>
vnoremap <c-]> g<c-]>
nnoremap g<c-]> <c-]>
vnoremap g<c-]> <c-]>

Tip: FZF also has a tag searching functionality

The junegunn/fzf.vim plugin provides two commands for quickly finding tags using the fzf fuzzy file finder:

  • :Tags — List tags in project.
  • :BTags — List tags in current buffer.

Include file list and definition lists

There are a couple of other lesser-known lists from Vim’s C-language heritage.

The include file list is populated by commands that look for a keyword in the current buffer and all “included” files. Which files are searched depends on:

  • The 'include' option, which identifies lines that include another file — this defaults to ^\s*#\s*include.

  • The 'path' option which tells Vim where to find said included files.

Search with:

  • :il[ist] {pattern} — search in [range] or file for pattern.

There’s also the definition list.

  • :dl[ist] {pattern} — search in [range] or file for pattern.

Since I primarily program in Python, this isn’t that useful; I use tag search (and the tag match list) to jump to symbol definitions. However I’ve learnt over time to never write-off Vim’s features — I’m sure this could be useful as a more focussed tag search when working in languages that use filepath includes.

More at :help include-search


The above is intended as a reference for myself, but hopefully it will be useful to others. The act of compiling this post has been incredibly useful; I’ve learnt many new things1 just fleshing out each section.

There’s a couple of key usage patterns that are worth calling out:

I need to jump somewhere

When editing code, you often need to jump to another location.

  • To jump to the definition of a function or class, use the tag list via :tag, :tjump or g CTRL-].

  • To examine everywhere that a function or class is being called or everywhere that matches a regex use the quickfix list via :grep (or, better, :Grep).

  • To jump to where you last were, use the jump list via CTRL-O.

  • To jump to where you last made a change, use the change list via g;.

  • To jump to where you were last in insert mode, use gi.

I need to apply the same transformation in lots of places

A powerful editing pattern comprises building a list of files or locations then applying the same editing operation to each entry. This is great for applying refactorings across a large codebase.

The best approach depends on how you want to build your list of files or locations:

  • If you want to select code locations, populate the quickfix list with :grep and apply the batch changes with :cdo.

  • If you want to select files by their content, populate the quickfix list with :grep and apply the batch changes with :cfdo.

  • If you want to select files by their filepath, populate the argument list with :args `fd $PATTERN` and apply the batch changes with :argdo.

  • If you want to select files manually, use :argadd and :argdelete to populate the argument list then use :argdo (this is generally preferable to using the buffer list and :bufdo, although that might suffice in some situations).

The editing operation could be an Ex command (e.g. %s/foo/bar/g | update), applying a macro (e.g. normal @q) or calling a bespoke Vimscript function (e.g. call MyBespokeFunction).


Something wrong? Suggest an improvement or add a comment (see article history)
Tagged with: Vim
Filed in: tips

Previous: A Vim mapping for opening virtualenv files
Next: Explaining

Copyright © 2005-2023 David Winterbottom
Content licensed under CC BY-NC-SA 4.0.