UP | HOME

Default emacs completions are good, I swear!

UPDATE [2023-02-28 Tue]: The bug described below with regards to minibuffer-choose-completion has been fixed in emacs 29.

As part of my use-the-least-amount-of-external-packages-so-you-can-learn-emacs-lisp mode of operation, I’m using the builtin completion system. That means no vertico, ivy, helm, selectrum, etc. I’ve been using it for a while and it’s perfect for me. I like to have a completions buffer instead of all the completion candidates appearing in the minibuffer, and the fact that it’s builtin means that it works with almost everything. I’ll get back to that later.

Emacs 29 introduced some functionality that made the task of making the default completion system usable much easier than before. You can read the NEWS file about it.

(setq completions-format 'one-column)
(setq completions-header-format nil)
(setq completions-max-height 20)
(setq completion-auto-select nil)

That’s really it. Well, almost. What we have here is a single column, vertically arranged *Completions* buffer that is never larger than 20 lines and isn’t auto selected when it’s opened up.

What I’m trying to do here is replicate some of the functionality of mct.el, my favourite completion framework. Its development was discontinued because of these new features introduced in emacs 29, and so I like to believe that mct, or at least its spirit, lives on through my own dotemacs config.

Another feature of mct.el was the ability to cycle through and choose completions from the completions buffer while keeping focus on the minibuffer or buffer if its completion at point. Emacs 29 has this by default with the M-<down> and M-<up> keybindings, but I’d like to change those to C-p and C-n, like our beloved mct.

(define-key minibuffer-mode-map (kbd "C-n") 'minibuffer-next-completion)
(define-key minibuffer-mode-map (kbd "C-p") 'minibuffer-previous-completion)

And for completion in region/at point as well:

(define-key completion-in-region-mode-map (kbd "C-n") 'minibuffer-next-completion)
(define-key completion-in-region-mode-map (kbd "C-p") 'minibuffer-previous-completion)

M-RET chooses a completion, but this doesn’t always work as of Oct 2022 . E.g in eshell, cd <TAB> opens the completions buffer and you can select a completion e.g a directory with C-n and C-p, but inserting it with M-RET gives the message: “Text is read-only”. I think this is because emacs is trying to insert the completion into the minibuffer, and I submitted a bug report about it.

But… wait a moment now, isn’t this emacs? Surely we can figure out why this is happening and fix it, right? Isn’t that the whole appeal? All you emacs folk talk about is its customizability, so go on then… customize it!

Well you’re in luck. I’ve done just that. Let’s go through it:

M-RET is bound to minibuffer-choose-completion, and here’s its definition:

(defun minibuffer-choose-completion (&optional no-exit no-quit)
  "Run `choose-completion' from the minibuffer in its completions window.
With prefix argument NO-EXIT, insert the completion at point to the
minibuffer, but don't exit the minibuffer.  When the prefix argument
is not provided, then whether to exit the minibuffer depends on the value
of `completion-no-auto-exit'.
If NO-QUIT is non-nil, insert the completion at point to the
minibuffer, but don't quit the completions window."
  (interactive "P")
  (with-minibuffer-completions-window
    (let ((completion-use-base-affixes t))
      (choose-completion nil no-exit no-quit))))

Here’s what I’ve rebound it to:

(defun my/minibuffer-choose-completion (&optional no-exit no-quit)
  (interactive "P")
  (with-minibuffer-completions-window
    (let ((completion-use-base-affixes nil))
      (choose-completion nil no-exit no-quit))))

(define-key completion-in-region-mode-map (kbd "M-RET") 'my/minibuffer-choose-completion)



Essentially the problem was the fact that the original function set the completion-use-base-affixes variable to t before calling choose-completion. The variable is defined as:

(defvar completion-use-base-affixes nil
  "Non-nil means to restore original prefix and suffix in the minibuffer.")

The prefix and suffix in this context is text preceding and following the place where the completion should be inserted. When non-nil, choose-completion is trying to

restore the original prefix and suffix in the minibuffer

however the minibuffer (when doing completion in region) is read only, so trying to insert anything returns the “Text is read-only” error. By jove, I think we’ve cracked it! My overriden my/minibuffer-choose-completion sets the variable to nil before calling choose-completion, so we’re all good now. Completion in region works as expected!

I know that the better practice is to advise this function instead of redefining it, but the problem is the inner let, and I only want to call this modified version when I am doing completion in region. Not sure how to do that with advices. I said I’m learning emacs lisp! Give me a break…

And that’s my completion system. Add these two external packages:

(require 'marginalia)
(marginalia-mode t)

(require 'orderless)
(setq completion-styles '(orderless))

and you’re good to go bro. You’re good to go…

Try it out, and let me know what you think! And if anyone knows how to implement this by advising minibuffer-choose-completion, please let me know!