Skip to main content

Customize completion-at-point

This post is an overview of text completion in Emacs using completion-at-point and how you can customize it. I will assume that you already know some Elisp and are familiar with the basics about variables and hooks. All examples also assume you use a fairly recent Emacs version (v.26 or later).

Usage

The completion-at-point command prompts the user with a list of completions for buffer text. Usually it is bound to M-TAB which is sometimes also bound to the wrapper command complete-symbol.

For editable buffers TAB is regularly used for indentation but you can also combine indentation and completion on TAB by configuring tab-always-indent:

;; First try to indent the current line, and if the line
;; was already indented, then try `completion-at-point'
(setq tab-always-indent 'complete)

The UI for the completion operation is determined by the value of completion-in-region-function. Completion frameworks like helm, ivy or selectrum provide a suitable function for this and set it automatically when you enable their modes.

Adding completions

The completion data gets gathered by running completion-at-point-functions. As discussed in a previous post you can add functions to hooks buffer locally. This is also what major modes and language server clients like eglot or lsp-mode do to provide their completions.

If the local hook fails to return any candidates the global hook runs. You can use this to add your own completions as a fallback. The functions of this hook receive no arguments and should return completion data containing the start and end positions of the text to be completed, a completion table and optionally some extra information (see the docstring of completion-at-point-functions for more details).

Here is useful example that adds file path completion to the hook:

(autoload 'ffap-file-at-point "ffap")
(defun complete-path-at-point+ ()
  "Return completion data for UNIX path at point."
  (let ((fn (ffap-file-at-point))
        (fap (thing-at-point 'filename)))
    (when (and (or fn (equal "/" fap))
               (save-excursion
                 (search-backward fap (line-beginning-position) t)))
      (list (match-beginning 0)
            (match-end 0)
            #'completion-file-name-table :exclusive 'no))))

(add-hook 'completion-at-point-functions
          #'complete-path-at-point+
          'append)

Afterwards completion-at-point will offer you file completions when your point is at a path. This is especially nice with selectrum which won’t exit file completions after each path level so you can conveniently navigate to the path like you would do with find-file.

The function above looks for a file path at point. If it finds one it returns the file path boundaries and the completion table. There will be more info about completion tables in the next section. The no value for :exclusive is added so that other completion-at-point-functions are continued to be queried if the completion table should fail.

You can use a similar pattern for any other completion functions you might want to add. See for example this stackexchange answer which shows how to add completions from a custom dictionary or this one which shows an example of adding dabbrev candidates as completions.

Completion tables

Completion tables can have many different formats. A list of strings, an alist, an obarray, a hash table or so called dynamic completion table which is a function. Those table formats can also be used by completing-read which is the function Emacs commands typically use to prompt a user with a list of completions.

There isn’t much to say about static tables so this section will look at the more interesting dynamic ones. The last example used the already existing completion-file-name-table function. You need a dynamic table for file completion because the returned file names will change based on the input path. Dynamic tables are also useful if you want to configure some custom completion behaviour which the table can control via its metadata. If you call the table with the metadata action argument it should return it, see (info "(elisp) Programmed Completion") for the full details.

The following example shows how you can add some metadata to completions. Emacs comes with a little helper function complete-with-action which you can use to handle the rest of the API so you only need to care about handling the metadata action:

(let* ((alist '(("GNU" . "GNU is not Unix")
                ("Emacs" . "Eight Megabytes And Constantly Swapping")))
       (annotf (lambda (str)
                 (format " (%s)" (cdr (assoc str alist))))))
  (completing-read "Candidates: "
                   (lambda (str pred action)
                     (if (eq action 'metadata)
                         `(metadata
                           (annotation-function . ,annotf)
                           (cycle-sort-function . identity)
                           (display-sort-function . identity))
                       (complete-with-action action alist str pred)))))

The example disables the default sorting behaviour by using the identity function. It also adds some annotations to describe the candidates in more detail. Note that there is also completion-extra-properties which can also be used to configure certain completion properties.

There are many more use cases where dynamic tables can be useful see for example this stackexchange answer which constructs a completion table with automatic caching.

Closing words

This post only talked about completion-at-point but there are many more useful ways to complete text in Emacs. One external package worth mentioning in the context of this post is the popular company package which supports auto completion.

Company also integrates with completion-at-point-functions: The variable company-backends includes the company-capf backend which provides you the completion candidates of completion-at-point-functions. There are also some extra properties you can add to your completion-at-point-functions which are picked up by company. You can inspect the source of elisp-completion-at-point or company-capf itself for more details on this.

See you in parendise!

Comments on Reddit