Auto-completion for Registers in Vim

Sean
5 min readJul 23, 2021

This is done by adding a function to the completefunc option that returns a list of your registers.

set completefunc=Registers

To make the function you have to return the register contents in a format that completefunc likes:

function! Registers(findstart, base)
if a:findstart == 1
return 0
endif
" Imagine, if you will, this is a full list
" of all your registers...
let l:regs = [ '"', '0', '1', '2', '3', '4', '5', '6', ...]
return {
\'words': map(l:regs,
\{ i, reg -> {
\'key': reg,
\'word': getreginfo(reg).regcontents[0],
\'abbr': '@' . reg,
\'menu': slice(trim(getreginfo(reg).regcontents[0]), 0, 20)
\}
\})
\}
endfunction

Add this to your vimrc and you’ll be able to insert the contents of your registers using CTRL-X CTRL-U in insert mode.

How it’s done

X Mode

Like most Vim devs I use a plugin to handle most of my completion needs, it just opens up automatically when I start typing and I use CTRL-N and CTRL-P to cycle through the options.

But did you know that Vim has a special X mode that can choose between all sorts of completion sources?

Try going into insert mode and pressing CTRL-X, you’ll see this message printed at the bottom of your view:

These are all the different X mode options.

You might already know CTRL-X CTRL-F which complete’s directory paths or CTRL-X CTRL-L which does whole line completion — it can choose from any whole line you’ve already written in an open buffer…

For our registers list I’m using CTRL-X CTRL-U which is the user defined completion menu. It’ll complete from a function you add to the completefunc option.

Function Format

You can learn all about the completefunc option format in :h complete-functions but it’s not a wild read so let me sum it up.

The function you add to completefunc has to accept two arguments findstart and base.

The function you add to completefunc will run twice. The first time it runs it’ll expect the index for where to start the menu. The second time it’ll expect the contents of the menu.

You can use findstart to tell you whether this is the first or second time the function is running. If its value is 1 it’s the first time and madly if it’s 0 it’s the second time, I know… #vimscriptingsmental.

So first of we write an if statement to check findstart and we return 0 to tell the menu to start at the first option:

function Registers(findstart, base)  " You have to prepend arg names with "a:"
if a:findstart == 1
return 0
endif
endfunction

You won’t need to base but you do have to add it because Vim doesn’t like you ignoring function args.

Completion Menu Format

Next we have to return our list of options in a special format. It has to be a dictionary (aka an object) with an array of dictionaries called words, which have three useful string parameters, word, abbr and menu:

{ 'words': [{ 'word': '', 'abbr': '', 'menu': '' }, ...] }
  • word — the text you want the option to be.
  • abbr — the label for the option in the dropdown.
  • menu — a pretty version of the text to display in the dropdown.

So next if the findstart arg is not 1 we’ll return our dictionary:

function Registers(findstart, base)
if a:findstart == 1
return 0
endif
return { 'words': [
\{
\'word': 'yanked stuff full version',
\'abbr': '@"',
\'menu': 'yanked stuff'
\},
\]}
endfunction

Getting reg contents

Vim provides in-built functions to get the contents of most common lists, for example getbufinfo() for :ls gives you data about all your buffers, getchangelist() for :changes, getjumplist() for :jumps and so on — you can browse through Vim’s functions with :h functions.

Annoyingly though I couldn’t find one for registers.

But it does have a function for showing you a register one at a time called getreginfo which accepts a register name and returns an object with a list called regcontents.

You can try it out by echoing it in command mode:

:echo getreginfo('"')

This’ll print an object with some info about the " register…

{'regcontents': ['yanked stuff'], 'regtype': 'v', 'points_to': '-'}

Don’t ask me why regcontents is a list.

Let’s get the actual contents of the " reg and put it into Registers:

function Registers(findstart, base)
if a:findstart == 1
return 0
endif
return { 'words': [
\{
\'word': getreginfo('"').regcontents[0],
" This adds @" to the start of the option so we can
" clearly see which reg it is...
\'abbr': '@"',
" I'm using slice() and trim() to remove whitespace
" and shorten the text for the menu
\'menu': slice(trim(getreginfo('"').regcontents[0]), 0, 20)
\},
\]}
endfunction

Mapping Registers

Last of all we have to do some mapping trickery to convert a full list of registers into the right format.

First make a list of register names:

" The "l:" make regs a "local" variable...
let l:regs = ['"', '1', '2', '3', '4', ...]

Then use Vim’s best function, map, to convert them…

map(l:regs, { index, reg -> {} })

Looks weird right? map can accept a lambda function (a bit like a javascript fat-arrow => function) that receives an index and then the value in the list. It returns whatever you tell it to for each item in regs.

Now let’s add the formatting code:

map(l:regs, { i, reg -> {  \'key': reg, 
\'word': getreginfo(reg).regcontents[0],
\'abbr': '@' . reg,
\'menu': slice(trim(getreginfo(reg).regcontents[0]), 0, 20)
\} })

Finally add that to Registers

function! Registers(findstart, base)
if a:findstart == 1
return 0
endif
let l:regs = [ '"', '0', '1', '2', '3', '4', '5', '6', ...] return {
\'words': map(l:regs,
\{ i, reg -> {
\'key': reg,
\'word': getreginfo(reg).regcontents[0],
\'abbr': '@' . reg,
\'menu': slice(trim(getreginfo(reg).regcontents[0]), 0, 20)
\}
\})
\}
endfunction

Press CTRL-X CTRL-U and you’ll see a list of your registers with their names prepended.

--

--