Built-in complete menu in Vim

Configuring Vim's native autocompletion feature

I've been using Vim for many years. However, my use-case has always been very simple: some note-taking, a bash or python script here and there, editing configuration files and not much more. I used Vim as a glorified Notepad. Over the years, I did start doing my own customisations, a short .vimrc, a colourscheme, but still nothing too exiting. Only recently, as I got into coding, I found myself trying to figure out how to do some extra things.

I use VS Code for some of my courses. One of the features that VS Code has and is very handy is code completion. After searching, I found out that Vim not only has a built-in completion feature, but it actually can be pretty handy.

Contents

Using the 'complete' menu

The most basic way to use the compete menu is to enter insert more, type one or more characters and press ctrl + n. This searches the sources configured in complete for keywords that start with the same characters you typed.

You can see these sources with :set complete?. If I start vim with vim -u NONE -U NONE -N -i NONE, so that it starts Vim without reading any configuration file, the default sources are: complete=.,w,b,u,t,i. According to :h 'complete', this means that:

  • It scans the current buffer (.)
  • It scans buffers from other Vim windows (w)
  • It scans loaded or unloaded buffers that are in the buffer list (b and u)
  • It scans tag files (see: :h tags) (t)
  • It scans current file as well as included files (i)

This is already quite powerful. In the simplest case, it will just look for words that start with the typed character. For example, if we have a simple text file that reads:

Hello, world
Hi everybody
HELP

And we type H in insert more and then Ctrl + N, it will autocomplete the word "Hello" and also show the words "Hi" and "HELP". We can cycle through the options with Ctrl + N for "Next" or Ctrl + P for "Previous". Or we can bail the autocompletion with Ctrl + E.

Let's try something a bit more interesting:

#include <stdio.h>

int main(void) {
    printf("Hello, world\n");
    return 0;
}

Now, if below the main function we type m and Ctrl + N, we will get "main" as first suggestion, and then every keyword starting from "m" that exist in stdio.h and all the libraries included by stdio.h, such as modify, min, malloc, etc and it will also display the path of the file where it found the matching keyword.

What about "tags"?

The use of tags is a way to index important keywords in a codebase. The ctags program, widely available in most Linux systems, can scan a directory and create an index with functions, structures, and other keywords present in the codebase. Vim, as well as other editors, can read these index files and locate the keyword and its definition.

In my system I have Exuberant Ctags installed by default. I hear Universal Ctags is a better alternative, actively maintained and supports several more languages. However, even my, pretty archaic, version of ctags supports several languages (you can check this with ctags --list-languages) and does a fairly decent job.

As an example, let's create the tags file in dmenu source code directory with ctags -R .. This command creates a tags file in the current directory formatted as tag_name<TAB>file_name<TAB>ex_cmd;"<TAB>extension_fields. Here, tag_name and file_name refer to the name of the tag and the file where it was found. ex_cmd refers to the search pattern that will be used to find the specific keyword. The extension_fields part gives some additional information, such as the kind of the object specified by the tag (for example, for C this can be function f, variable v, macro m, typedef t, etc) and the scope of the object (for example file).

Vim will automatically see files named tags or TAGS in the current directory (:set tags?) and it can be configured to look in different locations as well. To use the tags search, press Ctrl+]. Let's see an example: While browsing the dmenu.c file, in line 137 (for dmenu v5.2) we see the following function:

static int
drawitem(struct item *item, int x, int y, int w)
{
    if (item == sel)
        drw_setscheme(drw, scheme[SchemeSel]);
    else if (item->out)
        drw_setscheme(drw, scheme[SchemeOut]);
    else
        drw_setscheme(drw, scheme[SchemeNorm]);

    return drw_text(drw, x, y, w, bh, lrpad / 2, item->text, 0);
}

With the cursor on drw_setscheme, we can press Ctrl+] to jump to the definition of the drw_setscheme function in the drw.c file. Pressing Ctrl+o will take us back to the initial file and location.

As we saw, tags are also used by the 'complete' menu. Let's start typing drw_ and press Ctrl+x Ctrl+]. The complete menu will use the tags file and show all functions beginning with drw_ in the popup menu. A lot more information about tags can be found in the Learn Vim tags chapter and the Vim Tips Wiki.

Additional completion modes

Completion can also suggest entire lines of text. On insert mode, if we press Ctrl + x Ctrl+l, the popup menu will suggest either entire lines, if we are at the beginning of an empty line, or lines that start with the same characters as our current line. In fact, Ctrl + x in insert mode will display a menu of possible completion options.

One other interesting feature is the completion from dictionary words. If we enable spell checking (:set spell or :set spell spelllang=en), the complete menu will suggest corrections or modifications for the word under the cursor. It really doesn't work great, the suggestions aren't the best; however, it can be handy to quickly correct misspelled words.

Finally, Ctrl + x Ctrl+f triggers filename completion, a handy way to quickly include a file.

Omni complete

Another way to complete text in Vim is the Omni Complete feature. This is a filetype-specific completion, so it requires to enable the filetype plugin (:filetype plugin on). Then we can enable omni completion with :set omnifunc=syntaxcomplete#Complete. The completion menu can be invoked with Ctrl + x Ctrl + o.

Now, here is where it becomes really interesting: Some languages, such as CSS or JavaScript, omni completion works surprisingly well. For example, if I start typing b in a CSS file and press Ctrl + x Ctrl + o, the menu will show every property name that begins with b. Similar, if I start typing let myvar = document.g in a JavaScript file, the popup menu will display all the methods that start with g (getElementById, etc).

For c files, things get a bit more complex. According to the help file :h compl-omni-filetypes, C code requires a tags file. Vim documentation recommends creating a tags file of all system headers with ctags -R -f ~/.vim/systags /usr/include /usr/local/include and then :set tags+=~/.vim/systags.

Before doing this, however, something else we can try is to make sure that omnifunc is set to syntaxcomplete#Complete. According to this StackOverflow answer, the filetype plugin, when it detects a C language file, it loads the appropriate script after the buffer is loaded, and this script contains the line: setlocal ofu=ccomplete#Complete. If we specified the omnifunc in .vimrc, it will be overwritten. For whatever reason, the dedicated completion file for C doesn't work great. However, if we specifically specify :set omnifunc=syntaxcomplete#Complete in our C file (or follow the instructions from Stack Overflow linked above to create a custom filetype plugin for C), the omni complete feature somewhat works. Combined with a tags file, such as the one we generated earlier for the dmenu source, it can work ok-ish.

Apparently, omni completion is even more abysmal for Python.

Final notes

Generally, Vim's built-in autocompletion isn't as polished as in other text editors. I found that I like the standard Ctrl + n completion, and that tags-based completion has a potential to be quite useful, once I actually get better at using it. I was expecting more from "omni completion", but apparently it needs quite some work configuring it to become good enough.

I found that adding the options
:set completeopt+=menuone,noinsert
works better for me, forcing Vim's complete menu to show the popup even if there is only one option, and preventing it from inserting the first option automatically.