Enhancing icomplete-vertical-mode in Emacs

Cover Image for Enhancing icomplete-vertical-mode in Emacs
Rahul M. Juliato
Rahul M. Juliato
#emacs#icomplete# configuration

icomplete-vertical-mode is a popular built-in option for Emacs users who prefer a vertical display of completions, offering a more modern feel similar to packages like Vertico or Ivy. Beyond its default use, it can also handle in-buffer completions. However, its behavior in this context may feel counterintuitive if you're not familiar with it or if you're accustomed to more modern in-buffer completion systems like Company, Corfu, or Auto-Complete.

In this post, I’d like to revisit a discussion I initiated back in April 2024, which led to a quick patch you can apply to make icomplete-vertical work more seamlessly for in-buffer completions, bringing it closer to the style of company or corfu.

The issue

Specifically regarding in buffer behaviour of icomplete, let's explore some variations.

Let's supose this minimal config:

(use-package icomplete
  :bind (:map icomplete-minibuffer-map
              ("C-n" . icomplete-forward-completions)
              ("C-p" . icomplete-backward-completions)
              ("C-v" . icomplete-vertical-toggle)
              ("RET" . icomplete-force-complete-and-exit))
  :hook
  (after-init . (lambda ()
                  (fido-mode -1)
                  (icomplete-mode 1)
                  ;; (icomplete-vertical-mode 1)
                  ))
  :config
  (setq tab-always-indent 'complete)  ;; Starts completion with TAB
  (setq icomplete-delay-completions-threshold 0)
  (setq icomplete-compute-delay 0)
  (setq icomplete-show-matches-on-no-input t)
  (setq icomplete-hide-common-prefix nil)
  (setq icomplete-prospects-height 10)
  (setq icomplete-separator " . ")
  (setq icomplete-with-completion-tables t)
  (setq icomplete-in-buffer t)
  (setq icomplete-max-delay-chars 0)
  (setq icomplete-scroll t)
  (advice-add 'completion-at-point
              :after #'minibuffer-hide-completions))

In this example I'll be searching for completions to (setq... and hit TAB for completion.

With icomplete-mode the beginning of the line: icomplete

With icomplete-mode on some advanced column: icomplete

Now lets toggle comments in our configuration:

;; (icomplete-mode 1)
(icomplete-vertical-mode 1)

With icomplete-vertical-mode the beginning of the line: icomplete

With icomplete-vertical-mode on some advanced column: icomplete

As you can see, the icomplete-vertical-mode completion won't respect the cursor position, as it happens with its classic horizontal counterpart. It always starts at the beginning of the next line.

A Proposed Solution

Messaging the Emacs help mail list here, quickly got me some answers to this issue.

Zhengyi Fu suggested a patch to address this issue. The patch modifies icomplete.el to prepend spaces to the completion lines, aligning them with the cursor’s column. Here’s the patch:

Index: emacs/lisp/icomplete.el
===================================================================
--- emacs.orig/lisp/icomplete.el
+++ emacs/lisp/icomplete.el
@@ -913,6 +913,16 @@ icomplete--render-vertical
                 ((> (length scroll-above) (length scroll-below)) nsections)
                 (t (min (ceiling nsections 2) (length scroll-above))))
           lines))
+    (when icomplete--in-region-buffer
+      (let ((column
+            (with-current-buffer icomplete--in-region-buffer
+              (save-excursion
+                (goto-char (car completion-in-region--data))
+                (current-column)))))
+       (dolist (l lines)
+         (add-text-properties
+           0 1 `(display ,(concat (make-string column ?\s) (substring l 0 1)))
+           l))))
     ;; At long last, render final string return value.  This may still
     ;; kick out lines at the end.
     (concat " \n"

As noted by Eli Zaretskii, contrary to initial impressions, applying this patch does not require rebuilding Emacs from source. Instead:

• Locate icomplete.el in your Emacs installation.

• Apply the patch.

• Byte-compile the updated file using M-x byte-compile-file.

• Restart Emacs to load the updated behavior.

And as magic, we get: icomplete

Another example in a buffer that provides completions with eglot (Typescript LSP Server): icomplete-eglot

A hacky copy/paste & try it

Of course you can just go hacky and override this function with the applied patch, it is ugly, it is not recommended to do, but if you'd like to quickly try this behavior you could.

Here's a version that may work with Emacs 30.0.92, shh try it and delete, don't tell anyone ;)

(use-package icomplete
  :bind (:map icomplete-minibuffer-map
              ("C-n" . icomplete-forward-completions)
              ("C-p" . icomplete-backward-completions)
              ("C-v" . icomplete-vertical-toggle)
              ("RET" . icomplete-force-complete-and-exit))
  :hook
  (after-init . (lambda ()
                  (fido-mode -1)
                  ;; (icomplete-mode 1)
                  (icomplete-vertical-mode 1)
                  ))
  :config
  (setq tab-always-indent 'complete)  ;; Starts completion with TAB
  (setq icomplete-delay-completions-threshold 0)
  (setq icomplete-compute-delay 0)
  (setq icomplete-show-matches-on-no-input t)
  (setq icomplete-hide-common-prefix nil)
  (setq icomplete-prospects-height 10)
  (setq icomplete-separator " . ")
  (setq icomplete-with-completion-tables t)
  (setq icomplete-in-buffer t)
  (setq icomplete-max-delay-chars 0)
  (setq icomplete-scroll t)
  (advice-add 'completion-at-point
              :after #'minibuffer-hide-completions)

  ;; FIXME - this is actually an override of internal icomplete to provide
  ;;         in buffer on column completion
  ;;
  ;; As first suggested by Zhengyi Fu:
  ;; https://mail.gnu.org/archive/html/help-gnu-emacs/2024-04/msg00126.html
  ;;
  (defun icomplete--augment (md prospects)
    "Augment completion strings in PROSPECTS with completion metadata MD.
Return a list of strings (COMP PREFIX SUFFIX SECTION).  PREFIX
and SUFFIX, if non-nil, are obtained from `affixation-function' or
`annotation-function' metadata.  SECTION is obtained from
`group-function'.  Consecutive `equal' sections are avoided.
COMP is the element in PROSPECTS or a transformation also given
by `group-function''s second \"transformation\" protocol."
    (let* ((aff-fun (completion-metadata-get md 'affixation-function))
           (ann-fun (completion-metadata-get md 'annotation-function))
           (grp-fun (and completions-group
                         (completion-metadata-get md 'group-function)))
           (annotated
            (cond (aff-fun
                   (funcall aff-fun prospects))
                  (ann-fun
                   (mapcar
                    (lambda (comp)
                      (let ((suffix (or (funcall ann-fun comp) "")))
                        (list comp ""
                              ;; The default completion UI adds the
                              ;; `completions-annotations' face if no
                              ;; other faces are present.
                              (if (text-property-not-all 0 (length suffix) 'face nil suffix)
                                  suffix
                                (propertize suffix 'face 'completions-annotations)))))
                    prospects))
                  (t (mapcar #'list prospects)))))
      (if grp-fun
          (cl-loop with section = nil
                   for (c prefix suffix) in annotated
                   for selectedp = (get-text-property 0 'icomplete-selected c)
                   for tr = (propertize (or (funcall grp-fun c t) c)
                                        'icomplete-selected selectedp)
                   if (not (equal section (setq section (funcall grp-fun c nil))))
                   collect (list tr prefix suffix section)
                   else collect (list tr prefix suffix ))
        annotated)))

  (cl-defun icomplete--render-vertical
      (comps md &aux scroll-above scroll-below
             (total-space ; number of mini-window lines available
              (1- (min
                   icomplete-prospects-height
                   (truncate (max-mini-window-lines) 1)))))
    ;; Welcome to loopapalooza!
    ;;
    ;; First, be mindful of `icomplete-scroll' and manual scrolls.  If
    ;; `icomplete--scrolled-completions' and `icomplete--scrolled-past'
    ;; are:
    ;;
    ;; - both nil, there is no manual scroll;
    ;; - both non-nil, there is a healthy manual scroll that doesn't need
    ;;   to be readjusted (user just moved around the minibuffer, for
    ;;   example)l
    ;; - non-nil and nil, respectively, a refiltering took place and we
    ;;   may need to readjust them to the new filtered `comps'.
    (when (and icomplete-scroll
               icomplete--scrolled-completions
               (null icomplete--scrolled-past))
      (cl-loop with preds
               for (comp . rest) on comps
               when (equal comp (car icomplete--scrolled-completions))
               do
               (setq icomplete--scrolled-past preds
                     comps (cons comp rest))
               (completion--cache-all-sorted-completions
                (icomplete--field-beg)
                (icomplete--field-end)
                comps)
               and return nil
               do (push comp preds)
               finally (setq icomplete--scrolled-completions nil)))
    ;; Then, in this pretty ugly loop, collect completions to display
    ;; above and below the selected one, considering scrolling
    ;; positions.
    (cl-loop with preds = icomplete--scrolled-past
             with succs = (cdr comps)
             with space-above = (- total-space
                                   1
                                   (cl-loop for (_ . r) on comps
                                            repeat (truncate total-space 2)
                                            while (listp r)
                                            count 1))
             repeat total-space
             for neighbor = nil
             if (and preds (> space-above 0)) do
             (push (setq neighbor (pop preds)) scroll-above)
             (cl-decf space-above)
             else if (consp succs) collect
             (setq neighbor (pop succs)) into scroll-below-aux
             while neighbor
             finally (setq scroll-below scroll-below-aux))
    ;; Halfway there...
    (let* ((selected (propertize (car comps) 'icomplete-selected t))
           (chosen (append scroll-above (list selected) scroll-below))
           (tuples (icomplete--augment md chosen))
           max-prefix-len max-comp-len lines nsections)
      (add-face-text-property 0 (length selected)
                              'icomplete-selected-match 'append selected)
      ;; Figure out parameters for horizontal spacing
      (cl-loop
       for (comp prefix) in tuples
       maximizing (length prefix) into max-prefix-len-aux
       maximizing (length comp) into max-comp-len-aux
       finally (setq max-prefix-len max-prefix-len-aux
                     max-comp-len max-comp-len-aux))
      ;; Serialize completions and section titles into a list
      ;; of lines to render
      (cl-loop
       for (comp prefix suffix section) in tuples
       when section
       collect (propertize section 'face 'icomplete-section) into lines-aux
       and count 1 into nsections-aux
       when (get-text-property 0 'icomplete-selected comp)
       do (add-face-text-property 0 (length comp)
                                  'icomplete-selected-match 'append comp)
       collect (concat prefix
                       (make-string (- max-prefix-len (length prefix)) ? )
                       (completion-lazy-hilit comp)
                       (make-string (- max-comp-len (length comp)) ? )
                       suffix)
       into lines-aux
       finally (setq lines lines-aux
                     nsections nsections-aux))
      ;; Kick out some lines from the beginning due to extra sections.
      ;; This hopes to keep the selected entry more or less in the
      ;; middle of the dropdown-like widget when `icomplete-scroll' is
      ;; t.  Funky, but at least I didn't use `cl-loop'
      (setq lines
            (nthcdr
             (cond ((<= (length lines) total-space) 0)
                   ((> (length scroll-above) (length scroll-below)) nsections)
                   (t (min (ceiling nsections 2) (length scroll-above))))
             lines))
    ;;; ------- NON ORIGINAL HERE...
      (when icomplete--in-region-buffer
        (let ((column
               (with-current-buffer icomplete--in-region-buffer
                 (save-excursion
                   (goto-char (car completion-in-region--data))
                   (current-column)))))
          (dolist (l lines)
            (add-text-properties
             0 1 `(display ,(concat (make-string column ?\s) (substring l 0 1)))
             l))))
    ;;; -------- NON ORIGINAL ENDS HERE...
      ;; At long last, render final string return value.  This may still
      ;; kick out lines at the end.
      (concat " \n"
              (cl-loop for l in lines repeat total-space concat l concat "\n")))))

Is this patch the new default?

Not at all. As you might know, an initial "it works" does not mean it is fully tested and free of bugs, right?

I ended up not having the time to properly suggest some new features to icomplete, such as:

• Turning this "follow the cursor" feature on/off

• Providing a "callback" function where the user could customize what function is wanted to perform this action

• Providing a way to add markings, such as arrow indicators, prefixes, suffixes, or maybe icons

I thought I’d have the time, but it has been a while since April, and other projects with already delayed statuses took priority. I figured it might be a good idea to share with the community that this sort of in-buffer completion is already possible with no external packages, especially for those trying to keep their configurations more purist.

Conclusion

The enhancements to icomplete-vertical-mode presented in this post demonstrate how a small patch can significantly improve the in-buffer completion experience, making it more intuitive and aligned with modern expectations. While this solution isn't yet part of the default Emacs distribution, it shows the potential of leveraging existing tools and a bit of customization to achieve powerful results.

If you're an Emacs purist or just someone looking to simplify your configuration without relying on external packages, this approach is worth exploring. And who knows? With enough community interest, we might see such improvements integrated into Emacs in the future.

As always, feel free to share your thoughts, improvements, or challenges in implementing this solution. Collaboration is what makes the Emacs community thrive!