Skip to content

Instantly share code, notes, and snippets.

@g0xA52A2A
Last active August 20, 2021 22:18
Show Gist options
  • Save g0xA52A2A/5105e2f4539ba6700c237478325e8dc9 to your computer and use it in GitHub Desktop.
Save g0xA52A2A/5105e2f4539ba6700c237478325e8dc9 to your computer and use it in GitHub Desktop.

Switching buffers and opening files

Out of the box Vim allows us to pretty easily open a buffer on a partial match or make wildcard searches for files to edit. As illustrated along with some other things in this excellent post.

So we certainly don't need something like a flashy fuzzy matcher as we have the functionality built in. However what we do lack is the uniform interface. One of the more common patterns I find myself in is thinking if I want to use the :buffer or :edit command.

It would be nice to have a single command that can switch to a buffer or open an arbitrary path.

Recap

As a quick review, the :buffer command switches to an existing buffer. Whereas :edit opens a path that may or may not already have an associated buffer. For both their arguments may or may not already exist on disk. So it makes sense to allow :buffer to take a partial but uniquely matching argument. Whereas :edit needs its argument to be an actual path (or something that expands to one). Consequently :buffer and :edit get correspondingly different completions.

The :edit command is broadly capable of serving as the desired combination command with the exception of not being able to point to buffers without a backing file (not something I'm worried about needing). In addition for such a command I would value existing buffers over as-yet unopened paths.

Outline

As we wish to be able to open arbitrary files we cannot have :buffer's partial matching behaviour as it only has to deal with a finite list of buffers. Augmenting :edit's completion with buffers as well as files seems like simple way to give us a command that can take us to where we want to go nearly as quickly. Without first having to think which of :buffer or :edit is better suited.

So let's define a new :Edit command.

command! -bar -nargs=1 -complete=customlist,EditComplete Edit edit <args>

And a custom completion function to go with it. It will get lists of completions for buffers and files. It will return the list of buffers that matched followed by files that matched, resulting the behaviour of prioritising buffers ahead of paths as desired. Additionally we should filter out anything in the files list that already exists in the buffers list otherwise we may see duplicates.

function! EditComplete(ArgLead, CmdLine, CursorPos) abort
  let buffers = getcompletion(a:ArgLead, 'buffer', 1)
  let files   = getcompletion(a:ArgLead, 'file',   1)

  return buffers + filter(files, "index(buffers, v:val) == -1")
endfunction

Getting picky with Git

Preferring files already tracked by version control also seems like a reasonable choice. For Git we can use git ls-files to find matching files. As a precaution we'll redirect stderr to /dev/null so this doesn't blow up when used under a path that isn't tracked by Git.

If this should return an empty result we'll fall back to the standard path completion.

function! EditComplete(ArgLead, CmdLine, CursorPos) abort
  let buffers = getcompletion(a:ArgLead, 'buffer', 1)
  let git     = systemlist('git ls-files ' . a:ArgLead . ' 2> /dev/null')
  let files   = !empty(git) ? git : getcompletion(a:ArgLead, 'file', 1)

  return buffers + filter(files, "index(buffers, v:val) == -1")
endfunction

Feeling fuzzy

Though this works providing we start by typing paths as they exist we can be lazier and get back partial match behaviour more like :buffer by both prefixing and appending the pattern with * when doing git ls-files.

function! EditComplete(ArgLead, CmdLine, CursorPos) abort
  let buffers = getcompletion(a:ArgLead, 'buffer', 1)
  let pattern = shellescape('*' . a:ArgLead . '*')
  let git     = systemlist('git ls-files ' . pattern . ' 2> /dev/null')
  let files   = !empty(git) ? git : getcompletion(a:ArgLead, 'file', 1)

  return buffers + filter(files, "index(buffers, v:val) == -1")
endfunction

But we very much are trading specificity for laziness here.

Say we intend our argument to be a path to a new file. Should the path be a substring of a path tracked by git such that it matches one and only one path, the completion will expand to said path. Even if there are multiple matches for an input that's intended to be a new path, the potential "pollution" to your wildmenu might be something you don't want. When thinking about this it's also worth considering the value you have wildmode set to.

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