Enhancing icomplete-vertical-mode in Emacs
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:
With icomplete-mode
on some advanced column:
Now lets toggle comments in our configuration:
;; (icomplete-mode 1)
(icomplete-vertical-mode 1)
With icomplete-vertical-mode
the beginning of the line:
With icomplete-vertical-mode
on some advanced column:
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:
Another example in a buffer that provides completions with eglot
(Typescript LSP Server):
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!