EntriesAbout

Emacs Completes Me

Plus, a Small Helper

New Things

For no particular reason other than wanting to try something new, I’ve migrated away from Helm to a completion setup for Emacs based on Protesilaos’ latest config (see prot-minibuffer.el). That is, just using normal Emacs completion – albeit with some of the new goodies from Emacs 28 – along with Embark, Consult, and Orderless.

My Helm configuration was pretty well-honed and I’ve done a lot of stuff with custom actions, so I had a fairly high bar to be able to switch over. I’d want to easily be able to switch to buffers or files in a new window, controlling whether it would be a horizontal or vertical split; plus, I have a little package specifically for Helm to easily switch between multiple shells.

Splitting the Difference

It took a little bit, but I was able to replicate the open-in-split functionality I wanted with Embark. The one little wrinkle I encountered with writing my commands was handling being invoked from the minibuffer or the completion window. It took some experimentation, but I was ultimately able to put something fairly simple together:

(defun cogent--split-below (open-fn target)
  (select-window (split-window-below))
  (funcall open-fn target))
(defun cogent--split-right (open-fn target)
  (select-window (split-window-right))
  (funcall open-fn target))

(defun cogent--embark-act (fn &rest args) ;
  "Helper function to handle embark act events that can run from completion"
  (when (and (eq (selected-window) (active-minibuffer-window))
             (not (minibufferp)))
    (apply #'embark--quit-and-run fn args))
  (apply fn args))

(defun cogent/switch-to-buffer-horiz-split (buf)
  "Switch to buffer in a horizontal split"
  (interactive "BBuffer: ")
  (cogent--embark-act #'cogent--split-below #'switch-to-buffer buf))
(defun cogent/switch-to-buffer-vert-split (buf)
  "Switch to buffer in a vertical split"
  (interactive "BBuffer: ")
  (cogent--embark-act #'cogent--split-right #'switch-to-buffer buf))

(defun cogent/switch-to-file-horiz-split (file)
  "Switch to file in a horizontal split"
  (interactive "FFile: ")
  (cogent--embark-act #'cogent--split-below #'find-file file))
(defun cogent/switch-to-file-vert-split (file)
  "Switch to file in a vertical split"
  (interactive "FFile: ")
  (cogent--embark-act #'cogent--split-right #'find-file file))

(define-key embark-buffer-map (kbd "C-s") #'cogent/switch-to-buffer-horiz-split)
(define-key embark-buffer-map (kbd "C-v") #'cogent/switch-to-buffer-vert-split)
(define-key embark-file-map (kbd "C-s") #'cogent/switch-to-file-horiz-split)
(define-key embark-file-map (kbd "C-v") #'cogent/switch-to-file-vert-split)

The only wrinkle here is in the cogent–embark-act function. It has to detect if the minibuffer was active but the current buffer isn’t a minibuffer. From my fiddling, this seems to be the case when embark-act is invoked while scrolling in the *Completions* buffer.

Shell Games

The other big thing I needed to do replicate the functionality of my helm-switch-shell library, which is deep in my muscle memory now. I tend to have many *eshell* and *vterm* buffers in various project directories and this library makes it so I can usually switch to the one I want with just a keystroke or two. The idea is that it sorts the shell buffers by their distance from your current working directory, so the top of the list is usually the one you want. However, it also provides several helm actions on the candidates, to allow either switching to the buffer normally, switching in a horizontal or vertical split, or opening a new eshell or vterm in the same directory.

This was longer, but I was able to take much of the key code for sorting the candidates from my library and hack something together with normal completing-read, plus Marginalia and Embark, that gave me what I wanted. You can find that in my config here. I may eventually publish it stand-alone, since I already did so with my helm library, I suppose.

Jumping Around

The final thing that I wanted to share was a handy little helper function I wrote to make it easier to navigate the groups that are new to Emacs 28’s completion. Something that I got very used to with helm-rg was using the left and right arrow keys to skip through results by file, then use up and down to navigate through results for a single file and I wanted to be able to do that with this new system.

The following simple little function did it for me – I kind of expected something like this to already be built-in, but I suppose this is a fairly new addition, so the helper functions aren’t there yet.

(defun cogent/completion-next-group (&optional arg)
  "Move to the next completion group"
  (interactive "p")
  (while (and (not (eobp))
              (not (eq (point-max)
                       (save-excursion (forward-line 1) (point))))
              (get-text-property (point) 'completion--string))
    (next-line 1))
  (next-completion 1))

(defun cogent/completion-prev-group (&optional arg)
  "Move to the previous completion group"
  (interactive "p")
  (dolist (dir '(-1 1))
    (while (and (not (bobp))
                (not (eq 1
                         (save-excursion
                           (forward-line -1)
                           (line-number-at-pos))))
                (get-text-property (point) 'completion--string))
      (next-line -1))
    (unless (eq 1
                (save-excursion
                  (forward-line -1)
                  (line-number-at-pos)))
      (next-completion dir))))

(define-key completion-list-mode-map (kbd "<right>") #'cogent/completion-next-group)
(define-key completion-list-mode-map (kbd "<left>") #'cogent/completion-prev-group)

Hopefully something here was interesting or useful to someone out there!

Update 2021-07-28: After a conversation with Protesilaos, I’ve updated my completions functions to use the text-property-search-* family of functions; they now look like this:

(defun cogent/completion-next-group ()
  "Move to the next completion group"
  (interactive)
  (when-let (group (save-excursion
                     (text-property-search-forward 'face
                                                   'completions-group-separator
                                                   t nil)))
    (let ((pos (prop-match-end group)))
      (unless (eq pos (point-max))
        (goto-char pos)
        (next-completion 1)))))

(defun cogent/completion-prev-group ()
  "Move to the previous completion group"
  (interactive)
  (when-let (group (save-excursion
                     (text-property-search-backward 'face
                                                    'completions-group-separator
                                                    t nil)))
    (let ((pos (prop-match-beginning group)))
      (unless (eq pos (point-min))
        (goto-char pos)
        (text-property-search-backward 'face
                                       'completions-group-separator
                                       t nil)
        (next-completion 1)))))