Enhancing icomplete-vertical-mode in Emacs: A Follow-Up

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

In my previous post, I shared how I improved the usability of icomplete-vertical-mode by customizing its rendering to make completions more visually accessible. Since then, I’ve taken things a step further by generalizing the solution into a reusable function and introducing a range of customization options, including prefix markers and alignment controls. In this follow-up, I’ll walk you through these updates and show you how to get even more out of icomplete-vertical-mode.

In order to understand the Emacs original behavior and the history behind this, please take a look at my previous post before continue reading: Enhancing icomplete-vertical-mode in Emacs

Let's start with a little show and tell. The default config with this patch:

icomplete with custom prefix

Some customizations:

icomplete with another custom prefix

Key Features of the New Implementation

1. Prefix Markers for Selected and Unselected Candidates

To enhance visual clarity, I added customizable prefix markers for selected and unselected candidates. These markers make it easy to identify the current selection at a glance.

Customization Variables:

  • icomplete-vertical-selected-prefix-marker: A string used as the prefix for the currently selected completion candidate.

    Default: "» "

  • icomplete-vertical-unselected-prefix-marker: A string used as the prefix for unselected completion candidates.

    Default: " " (two spaces).

  • icomplete-vertical-selected-prefix-face: Controls the appearance of the selected prefix marker. Default: cyan text with bold weight.

  • icomplete-vertical-unselected-prefix-face: Controls the appearance of the unselected prefix marker. Default: gray text with normal weight.

2. Alignment for In-Buffer Completions

The alignment of in-buffer completions has been improved with the introduction of icomplete-vertical-in-buffer-adjust-list. When enabled, it aligns completions with the cursor position where the completion started, instead of defaulting to the first column. This makes the completions feel more natural and connected to the context.

  • icomplete-vertical-in-buffer-adjust-list:

    Default: t (enabled).

    If nil, completions will align to the first column as usual.

3. Toggleable Prefix Marker Rendering

The entire prefix marker functionality can be toggled on or off using icomplete-vertical-render-prefix-marker. If you prefer a cleaner list without markers, simply set this to nil.

4. Overridable Functions

Both new functions icomplete-vertical--adjust-lines-for-column and icomplete-vertical--add-marker-to-selected, can be overridden to implement new features. This is a little bit easier than re-write the entire icomplete--render-vertical function.

How do I get it?

We're going to explore two ways of applying this patch:

  1. Applying the patch to your Emacs source or package directory
  2. Overwriting the setup directly in your init.el

Let’s dive into both methods.

Method 1: Applying the Patch

You’ll need to locate the lisp/icomplete.el file in your system or in the Emacs source directory if you compile Emacs yourself. Follow these steps to apply the patch:

1.) Locate the File

Identify the location of icomplete.el on your system. If you're using a precompiled version of Emacs, you can find it under the lisp/ directory of your Emacs installation. For users compiling Emacs from source, look for the file in the Emacs source tree.

2.) Download the Patch

Download the patch file from here. Save it somewhere easily accessible.

3.) Apply the Patch with Git

Navigate to the directory containing icomplete.el and run the following command:

git apply path-to-patch-file.patch

Replace path-to-patch-file.patch with the actual file path to the downloaded patch.

4.) Load the Updated Version in Your init.el After applying the patch, use the following use-package configuration to load the updated version. The gif demo above uses the following configuration:

(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-vertical-mode 1)))
  :config
  (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)
              
  ;; These are our new post-patch options
  (setq icomplete-vertical-selected-prefix-marker "» ")
  (setq icomplete-vertical-unselected-prefix-marker "  ")
  (setq icomplete-vertical-in-buffer-adjust-list t)
  (setq icomplete-vertical-render-prefix-marker t))

Method 2: Overriding in init.el

This method involves directly overriding the necessary options in your init.el without applying the patch. While this is not the recommended approach, it can be used as a quick way to try out the changes.

  1. Add the Custom Code

    You can directly customize icomplete-vertical-mode with the following code in your init.el:

;;; ICOMPLETE
(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-vertical-mode 1)))
  :config
  (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)

  (defcustom icomplete-vertical-selected-prefix-marker "» "
    "Prefix string used to mark the selected completion candidate.
If `icomplete-vertical-render-prefix-marker' is t, the string
setted here is used as a prefix of the currently selected entry in the
list.  It can be further customized by the face
`icomplete-vertical-selected-prefix-face'."
    :type 'string
    :group 'icomplete
    :version "31")

  (defcustom icomplete-vertical-unselected-prefix-marker "  "
    "Prefix string used on the unselected completion candidates.
If `icomplete-vertical-render-prefix-marker' is t, the string
setted here is used as a prefix for all unselected entries in the list.
list.  It can be further customized by the face
`icomplete-vertical-unselected-prefix-face'."
    :type 'string
    :group 'icomplete
    :version "31")

  (defcustom icomplete-vertical-in-buffer-adjust-list t
    "Control whether in-buffer completion should align the cursor position.
If this is t and `icomplete-in-buffer' is t, and `icomplete-vertical-mode'
is activated, the in-buffer vertical completions are shown aligned to the
cursor position when the completion started, not on the first column, as
the default behaviour."
    :type 'boolean
    :group 'icomplete
    :version "31")

  (defcustom icomplete-vertical-render-prefix-marker t
    "Control whether a marker is added as a prefix to each candidate.
If this is t and `icomplete-vertical-mode' is activated, a marker,
controlled by `icomplete-vertical-selected-prefix-marker' is shown
as a prefix to the current under selection candidate, while the
remaining of the candidates will receive the marker controlled
by `icomplete-vertical-unselected-prefix-marker'."
    :type 'boolean
    :group 'icomplete
    :version "31")

  (defface icomplete-vertical-selected-prefix-face
    '((t :inherit font-lock-keyword-face :weight bold :foreground "cyan"))
    "Face used for the prefix set by `icomplete-vertical-selected-prefix-marker'."
    :group 'icomplete
    :version "31")

  (defface icomplete-vertical-unselected-prefix-face
    '((t :inherit font-lock-keyword-face :weight normal :foreground "gray"))
    "Face used for the prefix set by `icomplete-vertical-unselected-prefix-marker'."
    :group 'icomplete
    :version "31")

  (defun icomplete-vertical--adjust-lines-for-column (lines buffer data)
    "Adjust the LINES to align with the column in BUFFER based on DATA."
    (if icomplete-vertical-in-buffer-adjust-list
        (let ((column
               (with-current-buffer buffer
                 (save-excursion
                   (goto-char (car data))
                   (current-column)))))
          (dolist (l lines)
            (add-text-properties
             0 1 `(display ,(concat (make-string column ?\s) (substring l 0 1)))
             l))
          lines)
      lines))

  (defun icomplete-vertical--add-marker-to-selected (comp)
    "Add markers to the selected/unselected COMP completions."
    (if (and icomplete-vertical-render-prefix-marker
             (get-text-property 0 'icomplete-selected comp))
        (concat (propertize icomplete-vertical-selected-prefix-marker
                            'face 'icomplete-vertical-selected-prefix-face)
                comp)
      (concat (propertize icomplete-vertical-unselected-prefix-marker
                          'face 'icomplete-vertical-unselected-prefix-face)
              comp)))

  (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);
    ;; - 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
       for comp = (icomplete-vertical--add-marker-to-selected comp)
       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 0 (- max-prefix-len (length prefix))) ? )
                       (completion-lazy-hilit comp)
                       (make-string (max 0 (- 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))
      (when icomplete--in-region-buffer
        (setq lines (icomplete-vertical--adjust-lines-for-column
                     lines icomplete--in-region-buffer completion-in-region--data)))
      ;; 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")))))

Conclusion

With the patch applied or the custom configuration set in your init.el, you've successfully enhanced your icomplete-vertical-mode to support vertical alignment and customizable markers. These improvements offer a cleaner and more user-friendly completion interface in Emacs, making it easier to navigate long lists of completions.

Whether you prefer applying the patch directly to the source code or overriding settings in your init.el, both methods offer a flexible way to tailor icomplete-vertical-mode to your workflow. Experiment with the options that best suit your needs, and feel free to tweak the configuration further to make it even more personalized.

If you encounter any issues or have suggestions for further improvements, don't hesitate to reach out. Emacs is all about customization, and the possibilities are endless!