Selection and completion frameworks

Author

Marie-Hélène Burle

One of the reasons why I love working in Emacs is the ease to find and jump to files or specific locations in files. Whether it is reopening a recent document, jumping to a bookmark, hopping to a specific header, looking for an expression, it can all be done smoothly and with previews thanks to a powerful modern selection framework.

Another strength is the countless options to auto-complete text. The same modern framework can also be used here.

The backends

Emacs has a bookmark system, it can open files with M-x find-file, look for recently opened files with M-x recentf, search in a buffer with M-x isearch, switch to another open buffer with M-x switch-to-buffer, jump to previous positions in the mark ring, yank text from the kill ring, and countless other functionalities.

Using such functions directly works, but it doesn’t make for the best user experience. Accessing them via a frontend that expands the minibuffer, shows available options, narrows them down through incremental search, and offers previews is a huge improvement.

Multiple such frontends, increasingly powerful and/or efficient, have been developed over time. All of them are still available.

A history of completion frameworks in Emacs

In the minibuffer

A number of completions in Emacs happen in the minibuffer. Those are governed by the completing-read function.

By default, the minibuffer is a single line with no offer of available options. It is quite dry… but many packages have improved it.

First, came IDO (“Interactively DO things”), part of Emacs. It expands the minibuffer and shows options to choose from.

Then, the IDO vertical package made the list of options in the minibuffer vertical, which is a big visual improvement.

From oremacs

HELM came and revolutionized the Emacs world. It became so popular that replacements for many basic Emacs functions got written to work with the HELM frontend.

HELM doesn’t just expands the minibuffer, it turns it into a fully-fledged buffer for much improved functionality.

From oracleyue

Because HELM is such a heavy duty tool, it tends to be slow. It also requires rewrites for all of the common function. Ivy came about to bring the snappiness of IDO back. Optional Counsel & Swiper make it nicer with function rewrites.

In the editing buffer

In-buffer completions, governed by completion-at-point are completions that happen in the buffer itself.

By default, the available options are displayed in a *Completions* buffer that is quite clunky to navigate. A number of packages have instead allowed them to happen in small pop-ups.

First came auto-complete.

Then came company-mode.

A modern framework

In recent years, a new set of packages came about which integrate closely Emacs internal functions. Lightweight, they are incredibly fast. Each package works on its own and you can pick and choose which functionality you want.

List of packages

Minibuffer

In-buffer

  • corfu    Frontend completion UI
  • orderless   Backend completion style
  • cape    Backend completion functions

I will demo usage of these packages in class.

Configuration

These packages are extremely well documented and you will find in the READMEs all the information you need to install and configure them to your liking.

As an example, I am sharing here my own configurations (menus most kbds which rely on an exotic self-made system), including those for eglot—Emacs client for Language Server Protocol servers, Abbrevs—Emacs abbreviation system, yasnippet—a template system for Emacs, and copilot, an Emacs plug-in to GitHub Copilot.

It wouldn’t make much sense to copy-paste this to your init file blindly: instead, read careful the README of the various packages, decide which you want to use, and start with a minimal configuration based on the packages’ authors suggestions.

;; completion utilities

;; orderless
;; backend completion matching style (regexp, flex, initialism...)
(use-package orderless
    :straight t
    :custom
    (completion-styles '(orderless basic))
    (completion-category-overrides '((file (styles basic partial-completion))
                                     (eglot (styles orderless)))))  ; use orderless with eglot

;; embark
(use-package embark
    :straight t
    :init
    (setq prefix-help-command #'embark-prefix-help-command)
    :config
    (defun my-embark-bindings-global ()
      (interactive)
      (embark-bindings t))
    ;; hide the mode line of the embark live/completions buffers
    (add-to-list 'display-buffer-alist
                 '("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*"
                   nil
                   (window-parameters (mode-line-format . none))))
    (defun my-embark-kill (&optional arg)
      (interactive "P")
      (require 'embark)
      (if-let ((targets (embark--targets)))
          (let* ((target
                  (or (nth
                       (if (or (null arg) (minibufferp))
                           0
                           (mod (prefix-numeric-value arg) (length targets)))
                       targets)))
                 (type (plist-get target :type)))
            (cond
              ((eq type 'buffer)
               (let ((embark-pre-action-hooks))
                 (embark--act 'kill-buffer target)))))))
    :bind (("<f1> b" . my-embark-bindings-global)
           :map minibuffer-local-map
           ("C-;" . embark-dwim)
           ("C-SPC" . embark-act-all)
           ("C-," . embark-act)
           ("M-k" . my-embark-kill)))

(use-package embark-consult
  :hook
  (embark-collect-mode . consult-preview-at-point-mode))

;; marginalia
(use-package marginalia
    :straight t
    :init
    (marginalia-mode 1)
    :bind (:map minibuffer-local-map
                ("M-a" . marginalia-cycle)))

;; minibuffer completion

;; vertico
;; frontend for completion in minibuffer
(use-package vertico
    :straight t
    :init
    (vertico-mode 1)
    ;; config of display for each function
    (vertico-multiform-mode 1)
    :config
    (setq vertico-multiform-commands
          '((consult-line buffer)
            (consult-line-thing-at-point buffer)
            (consult-recent-file buffer)
            (consult-mode-command buffer)
            (consult-complex-command buffer)
            (consult-locate buffer)
            (consult-project-buffer buffer)
            (consult-ripgrep buffer)
            (consult-fd buffer)
            (telega-msg-add-reaction buffer)))
    (defun my-exit-keeping-point ()
      (interactive)
      (let ((location (with-minibuffer-selected-window (point-marker))))
        (run-at-time 0 nil #'consult--jump location)
        (exit-minibuffer)))
    :bind (:map vertico-map
                ("C-k" . kill-whole-line)
                ("C-u" . kill-whole-line)
                ("C-o" . vertico-next-group)
                ("<tab>" . minibuffer-complete)
                ("M-<return>" . minibuffer-force-complete-and-exit)
                ("C-<return>" . my-exit-keeping-point)))

;; save search history
(use-package savehist
    :init
    (savehist-mode 1))

;; consult
;; backend completion functions
(use-package consult
    :straight t
    ;; buffers, files, etc
    :config
    (defun my-get-major-mode-name ()
      (interactive)
      (message "`%s'" major-mode))
    (defalias 'consult-line-thing-at-point 'consult-line)
    (consult-customize
     consult-line-thing-at-point
     :initial (thing-at-point 'symbol)))
    
;; completion at point

;; corfu
(use-package corfu
    :straight t
    :init
    (global-corfu-mode 1)
    :bind (:map corfu-map
                ;; Configure SPC for separator insertion
                ("SPC" . corfu-insert-separator)
                ("<tab>" . corfu-next)
                ("C-n" . corfu-next)
                ("C-p" . corfu-previous)))

;; dabbrev
(use-package dabbrev
    :custom
    (dabbrev-ignored-buffer-regexps '("\\.\\(?:pdf\\|jpe?g\\|png\\)\\'"))
    :bind (("<tab>" . dabbrev-expand)
           ("; <tab>" . dabbrev-completion)))

;; cape
(use-package cape
    :straight t
    :bind ("C-'" . completion-at-point))

;; abbrev
(use-package abbrev
  :straight nil
  :config
  (setq-default abbrev-mode t)
  :bind ("C-c <tab>" . add-global-abbrev))

;; yasnippet
(use-package yasnippet
    :straight t
    :init
    (yas-global-mode 1)
    :config
    (setq yas-snippet-dirs '("~/.emacs.d/snippets"))
    :bind (("C-c y n" . yas-new-snippet)         ; y=yas, n=new
           ("C-c y e" . yas-visit-snippet-file)  ; y=yas, e=edit
           ("C-c y r" . yas-reload-all)          ; y=yas, r=reload
           ;; and rebind open-line (C-o)
           ("; C-o" . open-line)
           ;; when yas-minor-mode-map is active
           :map yas-minor-mode-map
           ("<tab>" . nil)
           ("C-o" . yas-expand)
           ;; during snippet completion
           :map yas-keymap
           ("<tab>" . nil)
           ("C-o" . yas-next-field-or-maybe-expand)))

;; yasnippet-capf
(use-package yasnippet-capf
    :straight t
    :after cape)

;; auto-yasnippet
(use-package auto-yasnippet
    :straight t
    :bind ("C-c y a" . aya-create))

;; eglot
(straight-use-package 'eglot-jl)

;; copilot
;; copilot dependency
(straight-use-package 'editorconfig)

(use-package copilot
    :straight (:host github :repo "copilot-emacs/copilot.el" :files ("dist" "*.el"))
    :bind (("C-8" . copilot-complete)
           :map copilot-completion-map
           ("C-j" . copilot-accept-completion)
           ("C-f" . copilot-accept-completion-by-word)
           ("C-t" . copilot-accept-completion-by-line)
           ("M-n" . copilot-next-completion)
           ("M-p" . copilot-previous-completion)))