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.
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
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:
filename
attempts to complete filenames
from the filesystem.
If the path is relative,
Kakoune searches both its current directory,
and the directory containing the file being edited.word=buffer
attempts to complete words
that already exist in the current buffer.
See the extra_word_chars
option below for details
of what constitutes a “word”.word=all
attempts to complete words
from all open buffers.line=buffer
attempts to complete entire lines
that already exist in the current buffer.line=all
attempts to complete entire lines
from all open buffers.option=NAME
(for some NAME
) includes the relevant completions
from the option named NAME
.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
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
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.
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 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
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.
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!”
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:
execute-keys -draft
means these keys wil execute in a “draft” context,
so if we move the cursor it won’t move the “real” cursor.
Also, even though we’re in an InsertIdle
hook,
a draft context always starts in normal mode.2b
moves the cursor to the beginning of the word being edited,
and then again to the previous word.
This means the user won’t get any suggestions
until they’ve typed at least one letter of the next word,
which matches how Kakoune completions normally work.s \Aeat<space>\z<ret>
asserts that the newly selected word is “eat”.
\A
and \z
mean that “eat” must be the entire word,
not just appearing somewhere inside it,
and the space is there because
the b
command always selects the space it moves over
as well as the actual word itself.execute-keys
command is complete,
the draft context is thrown away,
the cursor is back where it was,
and we continue to the completion generation code
(here, just a comment).s
command throws an error,
and we jump down to the catch
block below.catch
block,
we set the completions option
to an empty list,
signalling to Kakoune that we have nothing to suggest.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:
evaluate-commands -draft
creates a draft context as before,
although here we’re evaluating commands,
not executing individual keys.execute-keys h
moves the cursor left one cell, unsurprisingly.
If the user is typing at the end of the line,
the cursor will be on the newline character at the end,
and we want the cursor to be on the word-in-progress itself.
From the previous execute-keys
,
we know the user is at least one letter into the new word,
so this won’t take us back to the previous word.<a-i>w
selects the entire word,
excluding any whitespace around it.
That’s exactly the text our completions should replace.
Also,
these keys always result in a “forward” selection,
with the cursor at the right-hand end.<a-semicolon>
flips the selection around,
so that the cursor is at the left-hand end,
exactly where we want to anchor our completions.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.
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
}
}