Emoji completion in Vim
2014. 06. 29.

This is a short article for Vim users on OSX.

Beer :beer:

Did you know it was possible to display Unicode Emoji characters on OSX terminal? I first realized that looking at the output of Homebrew command.

:seenoevil: :hearnoevil: :speaknoevil:

vim-emoji

I was immediately stoked and went straight on to writing a small Vim plugin called vim-emoji for accessing Emoji characters with their names, and since then have used it to decorate my Vim screen. The plugin is extremely minimal that it provides only two functions, emoji#for(name) for returning the Emoji character with the given name, and emoji#list() for getting the full list of names of available characters.

Emoji auto-completion

One of the things we can do with these functions is to build Emoji auto-completion in Vim. GitHub and many other markdown renderers on Web display an Emoji character when its name surrounded by colons (e.g. :apple:) is found in the document. So whenever I was writing a Markdown document for those pages I found myself having to look for the name of the Emoji I had in my mind from emoji-cheat-sheet.com. So came the idea of Emoji auto-completion, which is now a part of vim-emoji. See how it works.

Setting it up is a no-brainer.

set completefunc=emoji#complete

Add the above line to your .vimrc, then you'll be able to use Emoji completion with CTRL-X CTRL-U. (e.g. :app<CTRL-X><CTRL-U> to find :apple:)

But you may not need it unless you're editing a Markdown file, so let's enable the completion locally on markdown FileType event.

augroup emoji_complete
  autocmd!
  autocmd FileType markdown setlocal completefunc=emoji#complete
augroup END

Enjoy! :tada:

Implementation

It was the first time of me implementing a custom auto-completion for Vim, so I think I'm going to share a bit about my experience. The process was pretty straightforward and :help complete-functions was the only reference I needed for getting it done.

All we have to do to implement a custom completion is to write a single function which should run in two phases each step differentiated by the first argument passed to it:

function! EmojiComplete(findstart, base)
  if a:findstart
    " Phase 1 - returns the column number to start auto-completion
  else
    " Phase 2 - returns the list of candidates
  endif
endfunction

Phase 1

In the first phase, we need to determine if it's possible to auto-complete the word just before the cursor. We check if the prefix ends with the pattern we're looking for (e.g. :app), and return the position of the matched part, so that Vim can then substitute the part with auto-completion candidates.

function! EmojiComplete(findstart, base)
  if a:findstart
    return match(getline('.')[0:col('.') - 1], ':[^: \t]*$')
  else
    " Phase 2 - returns the list of candidates
  endif
endfunction

So how does this work? Vim expects the function to return the column number where the completion should start, or when no completion can be done, a negative integer between -1 and -3. Luckily, match() can be used here without any additional processing since it returns -1 on no match, and zero-based index if match is found, which effectively tells Vim to start completion from just before the found pattern.

Phase 2

After the function returns a non-negative integer, Vim subsequently calls it with the first argument set to 0. In this second phase, the function should return the list of the candidates for the given prefix, a:base. This is trivial as we have the list of Emoji names, emoji#list(). We sort the list, take names that start with the given prefix, and surround each one of them with colons.

function! EmojiComplete(findstart, base)
  if a:findstart
    return match(getline('.')[0:col('.') - 1], ':[^: \t]*$')
  elseif empty(a:base)
    " For some reason, Vim calls the function with empty a:base,
    " even when we previously returned -1
    return []
  else
    return map(filter(sort(emoji#list()),
             \        'stridx(v:val, a:base[1 : -1]) >= 0'),
             \ "':'.v:val.':'")
  endif
endfunction

Improvements

So that was the basic implementation of Emoji completion. But wait, it is missing the nice Emoji preview on the completion popup. It's simple. Instead of returning a list of strings, you return a list of dictionaries with the additional kind field.

{ 'word': ':'.emoji.':', 'kind': emoji#for(emoji) }

One more thing to note is that we sort and filter the list, and transform the result every time (putting colons before and after each name). Let's try to remove unnecessary repetition using s: variable.

function! EmojiComplete(findstart, base)
  if a:findstart
    return match(getline('.')[0:col('.') - 1], ':[^: \t]*$')
  elseif empty(a:base)
    return []
  else
    if !exists('s:emoji_list')
      let s:emoji_list = map(sort(emoji#list()),
            \ "{ 'word': ':'.v:val.':', 'kind': emoji#for(v:val) }")
    endif
    return filter(copy(s:emoji_list), 'stridx(v:val.word, a:base) >= 0')
  endif
endfunction

Notice that we copy s:emoji_list, since filter() does in-place mutation.

The full implementation from vim-emoji, which is not very different from what was shown here, can be found in GitHub. Check it out if you're interested. Sometimes Emoji characters mess up the screen, so the implementation contains the code that redraws the window. :whale:

Chaining multiple completion functions

While writing Emoji completion, there's one thing that irked me about how Vim handles custom completion. To enable a custom completion, we set completefunc to the name of the function. This means basically, we can use only one type of completion at a time. It is completely plausible that one might want to complete HTML tags as well as Emoji expressions while working on a Markdown document, but emoji#complete() is only for Emojis. So what should we do?

We can think of a wrapper function that works with a list of completion functions. In the first phase, it checks if any of the given functions can complete the prefix, and if so, it remembers the function, and forwards calls to it in the following steps.

function! CompletionChain(findstart, base)
  if a:findstart
    " Test against the functions one by one
    for func in g:user_completion_chain
      let pos = call(func, [a:findstart, a:base])
      " If a function can complete the prefix,
      " remember the name and return the result from the function
      if pos >= 0
        let s:current_completion = func
        return pos
      endif
    endfor

    " No completion can be done
    unlet! s:current_completion
    return -1
  elseif exists('s:current_completion')
    " Simply pass the arguments to the selected function
    return call(s:current_completion, [a:findstart, a:base])
  else
    return []
  endif
endfunction

let g:user_completion_chain = ['emoji#complete', 'HTMLTagComplete']
set completefunc=CompletionChain

If each completion expects different prefix, for example : for Emojis, and < for tags, this scheme works pretty well. :sunglasses:

ยป capture | close