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?