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.
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.
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
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
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.