Screwtape's Notepad

Intro to Kakoune completions

One of the things that makes Kakoune different from other editors is how interactive it is. All modern text editors are interactive, in the sense that you can type text at them and have the text appear on screen, but Kakoune seems to be filled to bursting with completion menus and inline documentation.

If you want your Kakoune plugin to fit with the aggressively helpful nature of the rest of the editor, it’s pretty easy to add your own custom completions to the mix.

Completion configuration

Before we get to writing a custom completer, we should talk about the various options that influence how Kakoune’s completion works. You may be able to get the behaviour you need without writing a single line of code.

The “completers” option

The completers option controls where Kakoune looks for the completions it offers. It’s a “completer-list” option, which is effectively like a string list, except that the strings must follow a specific format. The details are covered in the :doc options documentation, but for completeness, here they are again:

The order of items in this option is very important. Kakoune only presents completions from the first completer in the list that provides a non-empty result. If your completer comes after a completer like word=all that’s very likely to generate results, your custom completions might never show up at all.

The “static_words” option

The static_words option guarantees certain words will be available to the word=buffer and word=all completers, whether or not any buffers actually contain those words.

It’s mostly useful for adding language keywords and names of builtin functions, to the completion list.

The “extra_word_chars” option

The extra_word_chars option allows punctuation characters to count as part of a “word” for the purposes of the word=buffer and word=all completers. It also affects the w, b, and e normal-mode commands.

If this option is empty, Kakoune behaves as though it contains an underscore, so if you set this option you should include an underscore unless you really don’t want it to count as a word character.

The option must be set at “buffer” scope to have an effect. In addition, in order for the word=all completer to suggest a word, it must be considered a word both in the buffer where it appears, and the buffer where the user is typing. The two buffers don’t need identical values for the extra_word_chars option, but they must both include at least the punctuation characters that are part of the word in question.

Custom completions

If none of Kakoune’s standard completion options provide the behaviour you want, it’s time to think about writing a custom completion.

A custom completion requires a few moving parts to be useful: an option, code to add the option as a completer, and a InsertIdle hook to populate the completion option with appropriate suggestions.

The completions option

The completions option is just a custom option whose type is completions. It’s also a good idea to declare it as hidden option, since users don’t need to interact with it directly. For example:

declare-option -hidden completions my_custom_completions

Adding a new completer

Having a new completions option doesn’t do anything unless the option is listed in completers. Completions are normally specific to a particular filetype, so it’s natural to install the completions option in the same filetype hook that sets up highlighting, etc.

It doesn’t need to be in a filetype hook, though, so to keep our example simple we’re going to install our completions globally:

set-option global completers option=my_custom_completions %opt{completers}

Note that we do not add our completer with the typical set-option -add command. If we used -add, our new completer would be placed at the end of the list, and Kakoune might never get around to checking them. The whole point of a custom completer is to produce completions that are more relevant than the ones Kakoune produces by default, so it makes sense to add them at the front of the list.

Populating the completions option

The completions option is more than just a list of insertable strings. Kakoune needs to know how much existing text the completion replaces, and supports displaying extra information about each completion.

The most basic completions content just puts additional text after the cursor, immediately where it is. For example:

set-option window my_custom_completions \
    "%val{cursor_line}.%val{cursor_column}@%val{timestamp}" \
    "fish||fish" \
    "frog||frog"

The first item tells Kakoune where the completions start. Here we set the start to the current position of the cursor, but the cursor doesn’t have to be in exactly that position for the completions to appear. For example, consider the following document:

I like to eat fh

Put Kakoune’s cursor on the “f” and execute the above command to set the completions option. Go to the end of the line and switch to insert mode, and (assuming the option is installed as a completer) Kakoune should suggest replacing all of “fh” with “fish”, not just inserting it at the current cursor position.

The other items are the completions that Kakoune offers. Rather than being simple strings, there’s three separate fields: the string to insert, a Kakoune command to execute when the string is inserted, and a formatted string to display in the menu. For example, consider these fancier completions:

set-option window my_custom_completions \
    "%val{cursor_line}.%val{cursor_column}@%val{timestamp}" \
    "fish|info -style menu Fish of the day is trout a la creme|fish" \
    "frog||frog {MenuInfo}Fried with garlic"

The “fish” item shows up as “fish” in the menu, but when the user selects it, a tool-tip-style info box appears next to it, saying “Fish of the day is trout a la creme”. The “frog” item has no tool-tip, but it does include extra information displayed in the “MenuInfo” face.

There’s one other optional part of the completions option syntax. The completion examples above are fine for a user adding fresh text at the end of the buffer, but if you’re editing an existing document, sometimes you want to replace existing text instead of inserting into it. For example, if you’re editing an email address with completions from your address book, selecting a completion should replace the entire address, rather than inserting a complete new address in the middle of the old one. Here’s another example document:

I like to eat flounder!

…and here’s another command to provide completions:

set-option window my_custom_completions \
    "%val{cursor_line}.%val{cursor_column}+8@%val{timestamp}" \
    "fish||fish" \
    "frog||frog"

Note the +8 just before the timestamp field in the header item. That’s the exact number of bytes in “flounder”. Put the cursor on the “f” in “flounder” and run the command, then put the cursor on the “l” and switch to insert mode.

You should get both the “frog” and “fish” completions, and if you select “fish” from the completion menu the text should change to “…eat fish!”. Without the +8 length field in the header, the result would be “…eat fishlounder!”

Dynamically generating completions

A fixed, specific set of completions is not terribly useful in general. Instead, we’d like to automatically generate completions appropriate for whatever the user is typing. Specifically for our example, we’d like to complete “fish” and “frog” after the word “eat”.

The first part of the puzzle is generating completions when they’re needed. That’s easy: Kakoune provides the InsertIdle hook that fires a little while (less than a second) after the user stops typing in insert mode, which is a splendid time to check for and generate completions:

hook global InsertIdle .* %{
    # ...completion generation code goes here...
}

Next, we want to generate completions if the user is typing or editing a word after “eat”. To achieve this, we make use of the fact that Kakoune’s s command will throw an error if the given regex does not match anywhere in the selection. Combined with a try %{ ... } catch %{ ... } block, this gives us a way to do different things depending on the context around the cursor:

try %{
    execute-keys -draft 2b s \Aeat<space>\z<ret>

    # ...completion generation code goes here...

} catch %{
    set-option window my_custom_completions
}

In this code fragment:

To generate the completions header, we need to know the location of the beginning of the word, its total length, and the timestamp of the buffer’s most recent edit. Luckily, we can move the selection inside another draft context and obtain all those values with %val{} expansions:

evaluate-commands -draft %{
    execute-keys h <a-i>w <a-semicolon>

    set-option window my_custom_completions \
        "%val{cursor_line}.%val{cursor_column}+%val{selection_length}@%val{timestamp}"
}

In this code fragment:

Finally, once we have populated the header item, we can fill in all our completions:

set-option -add window my_custom_completions "fish||fish"
set-option -add window my_custom_completions "frog||frog"

Note that we’re using set-option -add here, so we don’t clobber the header item we set previously. Also, we could add both completions in the same command, but in practice completions usually come from some kind of loop that considers candidates and processes them into Kakoune’s expected format one at a time.

A completer skeleton

Now that we’ve seen all the pieces that make up a simple completer, we can put them together to make a finished completer. Feel free to use this as a base for your own completion plugins:

declare-option -hidden completions my_custom_completions

set-option global completers option=my_custom_completions %opt{completers}

hook global InsertIdle .* %{
    try %{
        # Test whether the previous word is "eat". If it isn't, this
        # command will throw an exception and execution will jump to
        # the "catch" block below.
        execute-keys -draft 2b s \Aeat<space>\z<ret>

        evaluate-commands -draft %{
            # Try to select the entire word before the cursor,
            # putting the cursor at the left-end of the selection.
            execute-keys h <a-i>w <a-semicolon>

            # The selection's cursor is at the anchor point
            # for completions, and the selection covers
            # the text the completions should replace,
            # exactly the information we need for the header item.
            set-option window my_custom_completions \
                "%val{cursor_line}.%val{cursor_column}+%val{selection_length}@%val{timestamp}"
        }

        # Now we've built the header item,
        # we can add the actual completions.
        set-option -add window my_custom_completions "fish||fish"
        set-option -add window my_custom_completions "frog||frog"

    } catch %{
        # This is not a place to suggest delicious delicacies,
        # so clear our list of completions.
        set-option window my_custom_completions
    }
}