Have you ever thought about defining your own completion for particular things like emails, contact names or for something complex like tailwind classes or github usernames? Well, my goal on this post is to show you how to do it in a simple and concise way!
A demo first
As you can see on the video above it's quite a simple mechanism to define your
own completion, first you create a function that return an array of strings and
then you assigned to completefunc
option that accept a function reference and
after that you can trigger the completion by pressing Ctrl+x Ctrl+u
when on
insert mode. Now let's understand the structure of this function and write a
more exciting one!
Understanding the completion function structure
Here is the function used on the video above for example:
function! MyCompletion(findstart, base) abort
return ['something', 'to', 'complete', 'devto']
endfunction
The first argument findstart
it's a numeric argument and vim will call it with value 1
on the first execution of completion to find the column position of the current word (to position the completion popup for example), then on a second moment vim call this function again with a value 0
where it expects an actual lists of items.
That's why on the video I got an error saying
E745: Using a List as a Number
, because vim was expecting the column number at that point of the completion cycle.
The second argument base
is a string with all the previous completion matches grouped together, of course it's empty on the first execution and gradually will be incremented if the user keep pressing the completion keybind.
Writing the simple version with filter
Now that we understand the basic structure for a completion function let's
improve it a little bit with a filter! Basically we'll detect the substring
that the user typed and return the correct option first so it's easy to select
it.
Here is an example:
function! MyCompletion(findstart, base) abort
if a:findstart
return 0
endif
let s:matches = ['something', 'to', 'complete', 'devto']
if a:base->len() == 0
return s:matches
endif
return s:matches->matchfuzzy(a:base)
endfunction
The matchfuzzy
on this context is a default function of vimscript and help us get an fuzzy matching algorithm that give our simple completion function a lot of power, as shown below:
Last improvement on the basic example
If you mess with this function on your own setup you'll observe that exists a bug, to clarify and keep us on the same page i'll show the bug below:
As you can see on the video we can only complete the first word and then everything stop working, that's because we didn't return a proper integer value for findstart! that way vim can't find the start of the current word and provide correct base
value, this is quite simple to solve and we can increment it like this:
function! MyCompletion(findstart, base) abort
if a:findstart
let s:startcol = col('.') - 1
while s:startcol > 0 && getline('.')[s:startcol - 1] =~ '\a'
let s:startcol -= 1
endwhile
return s:startcol
endif
let s:matches = ['something', 'to', 'complete', 'devto']
if a:base->len() == 0
return s:matches
endif
return s:matches->matchfuzzy(a:base)
endfunction
On this version we're effective walking through the line and updating the startcol
variable accordingly, that way vim will always find our current incompleted word and provide correct arguments.
Writing a more exciting completion function
Well at this point I think you understand the structure and power of a custom completion function in vim, so let's spice things up and give a more complex example.
- Let's assume we have a list of emails on
/tmp/emails
like this:
cherry@gmail.com
mel@gmail.com
morgana@outlook.com
yaya@heart.com
huelder@insiide.com.br
daniel@heart.com
- We'll write a function that get the content of this file and return the emails using the fuzzy matching algorithm, this can be done with the following function:
function! EmailCompletion(findstart, base) abort
if a:findstart
let s:startcol = col('.') - 1
while s:startcol > 0 && getline('.')[s:startcol - 1] =~ '\a'
let s:startcol -= 1
endwhile
return s:startcol
endif
let s:emails = expand('/tmp/emails')->readfile()
if a:base->len() == 0
return s:emails
endif
return s:emails->matchfuzzy(a:base)
endfunction
- And voila! let's see it working?
Top comments (4)
One common problem with this approach is that
completefunc
accepts only one function.For example, say you want to complete GitHub Issue Numbers and GitHub PR Numbers and GitHub Repo Contributors.
The way I accomplish this today is with the help of a powerful, yet minimal, completion plugin:
vim-mucomplete
Reading mucomplete's docs, you will get a hint of how to accomplish this.
Note: mucomplete is a thin wrapper around vim's built-in completions +
:h complete()
Using GitHub Issues, PRs, and Contributors as an example, an initial implementation may look like this:
That's it.
Now, in any buffer filetype (thanks to
default
chain), we get GitHub Issue/PR number completion by typing#
and pressing<tab>
key and GitHub Contributor completion by typing@
and pressing<tab>
key.Mucomplete is very configurable. For example, instead of registering our custom completions in the
default
chain, we can register them for specific filetypes, like:Enjoy your custom completions!
Yeah this make a lot of sense, I also use mucomplete to configure my nested custom or default completion chains
Even for snippets using miniSnip
I completely agree, is by far the best for me personally
Very niceeee!!