Vim's Arglist as a File-Centric Todo List

I often find myself wanting to make similar edits to a set of files within a project. More than I can do with sed, but simple enough that ag + vim are the perfect tools for the job. In the age-old tradition of using blog posts as notes to one’s future self (and any other kind soul who happens to need the same pile of hacks I stumbled upon), here we are.

Further below is an explanation of the different tools at play, but in short, this is the workflow:

  1. Open Vim with the desired files, e.g., vim $(ag -l "from 'react'")
  2. Edit the current file as needed using all the flexibility of Vim (macros, plugins, brute force edits all welcome here)
  3. When the edits for the current file are done, run the :ThankYouNext command (see below for the bit of Vimscript that makes that work) to move on to the next file.
  4. Repeat from step 2 until all files are edited and you’re left with an empty Vim session.

Populating the Arglist

This workflow uses Vim’s arglist feature, which allows us to manage a subset of the open buffers as a distinct group. In our case, we can use it as a file-centric todo list.

Starting from the command line, we can use any and all of our favorite tools like find, grep, sed, awk, ag (or ripgrep or ack or whichever you search with), fd, fzf, etc. Here’s a handful of examples:

# Files matching a search
❯ vim $(ag -l "from 'react-dom'")

# Files w/ names matching a pattern
❯ vim $(fd '_spec.rb$' spec/)

# Files selected from fuzzy finding w/ fzf
❯ vim $(fzf --multi)

# Files with uncommitted changes
❯ vim $(git diff --name-only)

# Files modified on this branch
❯ vim $(git diff master --name-only)

# Files w/ failing specs (using RSpec's persisted failure file)
❯ vim $(grep 'failed' spec/examples.txt | awk '{print $1}' | sed 's/\[.*$//' | sort | uniq)

# Conflicted files in git
❯ vim $(git ls-files -u | awk '{print $4}' | sort -u)

# A list of files copied to our clipboard
❯ vim $(pbpaste)

When we open Vim by providing an initial set of files, Vim will load them into the arglist and open the first file for editing. You can check the current state of the arglist at any time by running :args, with the output looking something like the following:

[dir/file_a.txt] dir/file_b.txt dir/nested/file_last.txt

Many thanks to Drew Neil for the amazing Vimcasts series, in particular this video on the arglist which got me started down this path.

Note: While it’s possible to populate the arglist from within Vim, I found that approach to have a number of edge cases and instead I’ve had the best luck starting from the command line.

Other Note: All of the above examples use vim $(...) style command substitution as I think it looks a little more obvious, but thanks to a tip from Rune Lausen I’ve learned that you can use ... | xargs -o vim (where ... is any of the above commands above like git diff --name-only). This is likely what I’ll end up using in my day to day as it makes it easier to iterate on the file-finding commands, and then just tack on the | xargs -o vim to pipe the files to Vim.

Working Through the List

This is the novel part that I always find myself forgetting, which brings us to this very blog post. Once we’re done with the edits for a given file, we want to write the current file, remove it from the list and progress to the next one.

To wrap up the handful of Vimscript functions needed to do this, you can add the following to your ~/.vimrc:

function! s:ThankYouNext() abort
  update
  argdelete %
  bdelete
  if !empty(argv())
    argument
  endif
endfunction

command! ThankYouNext call <sid>ThankYouNext()

With this in place we can now use the new command, :ThankYouNext, and we’ll be taken to the next file or an empty Vim session to let us know we’re done.

Solid code, the right features, and a strong team

I would love to help you build your platform, empower your team, and tackle everything in between. Let's talk!