/compdef

Primary LanguageEmacs Lisp

compdef.png

A stupid Emacs completion definer.

Overview

Paraphrased from these two reddit posts:

In-buffer completion in Emacs is handled via the completion-at-point command by default. It gathers the completion data from completion-at-point-functions (capf for short).

Company — a popular external package with similar functionality — uses company-backends to configure sources for completion data. Because company is designed to work with capf you can make use of it by adding the company-capf backend to company-backends.

However, company dumps all of your completion backends into a single global value of company-backends. Then it searches through them until one works out for your current mode. Sometimes this works, sometimes it doesn’t (there is often a conflict between backends, or there are too many active backends for snappy feedback). completion-at-point does the same with capf. compdef sets backends as a local variable for that specific mode, so you’re always pinging the right ones in the right order.

So by running the following:

(compdef
 :modes #'org-mode
 :company '(company-dabbrev company-capf)
 :capf #'pcomplete-completions-at-point)

You set company-backends and capf buffer locally for org-mode buffers (the global values might still get used, if the buffer local ones don’t make the completion gathering process stop).

You can now use completion-at-point command in org-mode, and pcomplete-completions-at-point will be used to get completion candidates. This works because pcomplete is already setup correctly for org-mode, and pcomplete-completions-at-point is pcomplete’s compatabilty interface for completion-at-point.

If you invoke company-complete (manually or via the timer) company uses company-dabbrev and company-capf as backends. The latter uses uses whatever is configured as capf, which is pcomplete-completions-at-point in this example.

All of this will work without any interference from external completion configurations.

Motivation

We keep reinventing the wheel on how to set local completion backends. Even the official company documentation (in this case company-yasnippet) recommends ugly workarounds like this:

(add-hook 'js-mode-hook
          (lambda ()
            (set (make-local-variable 'company-backends)
                 '((company-dabbrev-code company-yasnippet)))))

compdef sets local completion backends for both completion-at-point and company simultaneously, with some auto-magic thrown in for convenience (setting backends for multiple modes at the same time, mixing-and-matching modes and hooks, etc.). compdef is intentionally stupid, simply setting the the relevant variables to what you tell it to, and letting completion-at-point and company handle the finesse of interpreting them. I’ve seen some really powerful solutions to this problem, but they all seem to assume a certain approach to configuring completions and are thus usually embedded in a starter kit like Doom Emacs, Spacemacs… compdef isn’t that clever. It just works.

… Did I mention the use-package keywords?

Examples

completion-at-point

(compdef
 :modes '(emacs-lisp-mode lisp-interaction-mode ielm-mode)
 :capf '(helm-lisp-completion-or-file-name-at-point
         ggtags-completion-at-point
         t))

company

;; Add org keyword completions.
(compdef
 :modes #'org-mode
 :company '(company-dabbrev company-capf)
 :capf #'pcomplete-completions-at-point)

use-package

;; infer modes/hooks from package name
(use-package go-mode
  :capf go-complete
  :company company-go)

;; override
(use-package go-mode
  ;; mix-and-match hooks and modes freely
  :compdef go-mode my-custom-hook my-custom-mode
  :capf go-complete
  :company company-go)

;; same as above
(use-package go-mode
  :compdef (go-mode my-custom-hook my-custom-mode)
  :capf go-complete
  :company company-go)