Async autocompletion in Emacs
Recently I wrote company-mode
backend for Elixir - company-elixir
, which uses IEx in the background to fetch completions from.
Writing async backends for company-mode
is not documented very well. In this post, I’ll give a brief instruction on writing an async backend.
Reasons behind company-elixir
You can skip this section if you are only interested in the emacs part.
You may ask why do you need IEx for completions when there are standalone implementations for this? There is one reason:
- Completions in IEx are very handy and they work perfectly because it’s a part of the Elixir language. So it’s constantly being improved and maintained. On the other hand, custom implementations that provide completion functionality are being developed by enthusiasts in their free time.
But it also has its disadvantages:
- Autocompletion modules for IEx are not documented and it seems they’re not meant to be used outside of IEx. But it’s not a problem of
company-elixir
users. - Elixir application has to be in the correct state, so it can be compiled to run IEx. If your project can not be compiled, obviously completions won’t work.
- Running IEx as a separate process can be overhead if your application starts processes that do heavy work.
But I’ll definitely check elixir-lsp
out after I’ll be done with company-elixir
.
company-mode
backend function
In order to implement a company-mode
backend, you should define a single function with signature
(command &optional arg &rest ignored)
In different situations company-mode
will call it with different command
and arg
parameters so you have to define handlers for a specific command if you want to process it.
Let’s check how I defined it for company-elixir
:
(defun company-elixir (command &optional arg &rest ignored)
"Completion backend for company-mode."
(interactive (list 'interactive))
(cl-case command
(interactive (company-begin-backend 'company-elixir))
(prefix (and (eq major-mode company-elixir-major-mode)
(company-elixir--get-prefix)))
(candidates (cons :async
(lambda (callback)
(setq company-elixir--callback callback)
(setq company-elixir--last-completion arg)
(company-elixir--find-candidates arg))))))
It defines three handlers:
- The function has to be interactive and call
company-begin-backend
to init your backend when called interactively. - If the command is
prefix
, the function should return the expression that will be completed. - Finally,
candidates
command should return completions for the expression returned inprefix
.
Let’s look further into candidates
handler.
Async candidates
Steps to define async company-mode
backend:
I. To define async fetching of completions we have to return cons of :async
with the handler lambda which will return candidates, lambda should have a single parameter callback
. If you wanted to define synchronous handler instead of returning cons you should return just completions.
II. Save the callback that was passed to lambda to a global variable as I did in the example above:
(defvar company-elixir--callback nil "Company callback to return candidates to.")
...
(setq company-elixir--callback callback)
...
Or you can just pass this callback to the function that will return completions.
I used a global variable because I couldn’t pass a callback to it since I’m using a separate process to fetch completions, the output from this process is sent to my process filter.
You can find more about process filters here.
III. Return calculated completions by calling the saved callback with your completions
(funcall company-elixir--callback completions)
Conclustion
Hopefully, this post gave you a basic understanding of async company-mode
backends.
You can also check company-backends
variable documentation. It seems like it is the only documentation piece for company-mode
.
Comments