DEV Community

Cherry Ramatis
Cherry Ramatis

Posted on

How to create your own completion for vim

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

demo video
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

demo video about completion with fuzzy

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:

demo video about completion bug

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
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. And voila! let's see it working?

final demo with email completion

Top comments (4)

Collapse
 
pbnj profile image
Peter Benjamin (they/them)

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:

" get issue and pr numbers using `gh`, join them into 1 list, pass it to `complete()`
function! GHIssuesCompletion() abort
        call complete(col('.'), extend(systemlist('gh issue list --json number --jq .[].number'), systemlist('gh pr list --json number --jq .[].number')))
        return ''
endfunction

" get list of contributors using `gh`, pass it to `complete()`
function! GHUsersCompletion() abort
        call complete(col('.'), systemlist('gh api repos/{owner}/{repo}/contributors --jq .[].login'))
        return ''
endfunction

" register `#` as the trigger for `ghi` (arbitrary name)
" register `@` as the trigger for `ghu` (arbitrary name)
let g:mucomplete#can_complete = {}
let g:mucomplete#can_complete.default = {}
let g:mucomplete#can_complete.default.ghi = { t -> t is# '#' }
let g:mucomplete#can_complete.default.ghu = { t -> t is# '@' }

" register `ghi` & `ghu` keys to call the correct completion functions
let g:mucomplete#user_mappings = {}
let g:mucomplete#user_mappings.ghi = "\<c-r>=GHIssuesCompletion()\<cr>"
let g:mucomplete#user_mappings.ghu = "\<c-r>=GHUsersCompletion()\<cr>"

" register `ghi` and `ghu` keys as "default" completion options 
let g:mucomplete#chains = {}
let g:mucomplete#chains.default = [ 'ghi', 'ghu', 'path', 'omni', 'c-n', 'user', 'tags']
Enter fullscreen mode Exit fullscreen mode

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:

let g:mucomplete#chains.gitcommit = [ 'ghi', 'ghu', 'c-n', 'tags']
Enter fullscreen mode Exit fullscreen mode

Enjoy your custom completions!

Collapse
 
cherryramatis profile image
Cherry Ramatis

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

Collapse
 
cherryramatis profile image
Cherry Ramatis

I completely agree, is by far the best for me personally

Collapse
 
foxgeeek profile image
Foxgeeek

Very niceeee!!