Subject: bug#75794: [PATCH] feat(icomplete): markers and vertical alignment

From: Rahul Martim Juliato <[email-masked]>
Date: Tue, 04 Mar 2025 11:03:52 -0300

Great news Eli!!!

It took me a while, but I believe I’ve figured it out!

I made a video comparing the behavior before and after applying this
patch. I think a visual demonstration is more effective than words
alone, as it highlights the many visual improvements.

Here’s the video link: https://www.youtube.com/watch?v=ni0l6PruJEs

This patch introduces two new features that improve
icomplete-vertical-mode, which are explained below:

1.) Improve feature provided by `icomplete-in-buffer'.
    If user, besides setting `icomplete-in-buffer' to t, also set the
    new `icomplete-vertical-in-buffer-adjust-list' to t, the following are fixed/ improved:

Without the new `icomplete-vertical-in-buffer-adjust-list':

* wrapped lines   - completion candidates on different columns always shows candidates at column 0
* wrapped lines   - completion candidates on different lines always shows candidates at column 0
* wrapped lines   - completion candidates close to the end of buffer won't be printed
* truncated lines - completion candidates on different columns always shows candidates at column 0
* truncated lines - completion candidates on horizontally scrolled windows won't appear on buffer
                        as they're on column 0
* truncated lines - completion candidates close to the end of buffer wont be shown

With the new `icomplete-vertical-in-buffer-adjust-list':

* wrapped lines   - fix    : completion candidates on different columns will always be printed
                                 under the cursor
* wrapped lines   - feature: completion candidates on different columns close to the end
                                 of the buffer will adjust so they stay visible
* wrapped lines   - fix:   : completion candidates on different lines always be printed under
                                 the cursor
* wrapped lines   - fix    : if icomplete-prospects-height won't fit from current line to the
                                 end of vertical space, our window will be scrolled so we have at
                                 least this amount of lines. This ensures our candidates list is
                                 always visible
* truncated lines - fix    : completion candidates on different columns will always be printed
                                 under the cursor
* truncated lines - feature: completion candidates on different columns close to the end
                                 of the buffer will adjust so they stay visible even when we scroll
                                 horizontally
* truncated lines - feature: completion candidates on horizontally scrolled windows will be
                                 printed under the cursor
* wrapped lines   - feature: if icomplete-prospects-height won't fit from current line to the
                                 end of vertical space, our window will be scrolled so we have at
                                 least this amount of lines. This ensures our candidates list is
                                 always visible
* from wrapped    - feature: if we are on wrapped lines and manually horiontal scroll, the lines
      to truncated               will become automatically truncated, in this case, all the features
                                 above still works from either mode (wrapped or truncated).

Attached is the latest (and hopefully final) version of this patch.

>From 22fc5e56cd57c5e23b39f6de91831e73ae648515 Mon Sep 17 00:00:00 2001
From: Rahul Martim Juliato <[email-masked]>
Date: Tue, 4 Mar 2025 01:35:32 -0300
Subject: [PATCH] Enhance 'icomplete-vertical-mode'

New user options have been added to enhance 'icomplete-vertical-mode':

 + 'icomplete-vertical-in-buffer-adjust-list': Aligns in-buffer
completion to the original cursor column.
 + 'icomplete-vertical-render-prefix-indicator': When enabled, adds a
prefix indicator to completion candidates.
 + 'icomplete-vertical-selected-prefix-indicator': Specifies the prefix
string for the selected candidate.
 + 'icomplete-vertical-unselected-prefix-indicator': Specifies the prefix
string for unselected candidates.

New faces introduced:

 + 'icomplete-vertical-selected-prefix-indicator-face': Controls the
appearance of the selected candidate prefix.
 + 'icomplete-vertical-unselected-prefix-indicator-face': Controls the
appearance of unselected candidate prefixes.

* etc/NEWS: Document the new user options and faces.
* lisp/icomplete.el(icomplete-vertical-mode): Implement the new options
---
 etc/NEWS          |  19 ++++++++
 lisp/icomplete.el | 121 ++++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 137 insertions(+), 3 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index 67f9ed84bdf..7d180b5b04d 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -268,6 +268,25 @@ return value windows whose buffers share their text with BUFFER-OR-NAME.
 With such an entry, 'display-buffer-reuse-window' may also choose a
 window whose buffer shares text with the buffer to display.
 
++++
+*** New user options for 'icomplete-vertical-mode'.
+New user options have been added to enhance 'icomplete-vertical-mode':
+
+ - 'icomplete-vertical-in-buffer-adjust-list': Aligns in-buffer
+    completion to the original cursor column.
+ - 'icomplete-vertical-render-prefix-indicator': When enabled, adds a
+    prefix indicator to completion candidates.
+ - 'icomplete-vertical-selected-prefix-indicator': Specifies the prefix
+    string for the selected candidate.
+ - 'icomplete-vertical-unselected-prefix-indicator': Specifies the prefix
+    string for unselected candidates.
+
+New faces:
+
+ - 'icomplete-vertical-selected-prefix-indicator-face': Controls the
+    appearance of the selected candidate prefix.
+ - 'icomplete-vertical-unselected-prefix-indicator-face': Controls the
+    appearance of unselected candidate prefixes.
 
 ** Frames
 
diff --git a/lisp/icomplete.el b/lisp/icomplete.el
index c58bffbb36b..246da70a8ba 100644
--- a/lisp/icomplete.el
+++ b/lisp/icomplete.el
@@ -115,6 +115,18 @@ Otherwise this should be a list of the completion tables (e.g.,
   "Face used by `icomplete-vertical-mode' for the section title."
   :version "28.1")
 
+(defface icomplete-vertical-selected-prefix-indicator-face
+  '((t :inherit font-lock-keyword-face :weight bold :foreground "cyan"))
+  "Face used for the prefix set by `icomplete-vertical-selected-prefix-indicator'."
+  :group 'icomplete
+  :version "31.1")
+
+(defface icomplete-vertical-unselected-prefix-indicator-face
+  '((t :inherit font-lock-keyword-face :weight normal :foreground "gray"))
+  "Face used for the prefix set by `icomplete-vertical-unselected-prefix-indicator'."
+  :group 'icomplete
+  :version "31.1")
+
 ;;;_* User Customization variables
 (defcustom icomplete-prospects-height 2
   ;; We used to compute how many lines 100 characters would take in
@@ -166,6 +178,46 @@ will constrain Emacs to a maximum minibuffer height of 3 lines when
 icompletion is occurring."
   :type 'hook)
 
+(defcustom icomplete-vertical-in-buffer-adjust-list nil
+  "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.1")
+
+(defcustom icomplete-vertical-render-prefix-indicator nil
+  "Control whether a indicator is added as a prefix to each candidate.
+If this is t and `icomplete-vertical-mode' is activated, a indicator,
+controlled by `icomplete-vertical-selected-prefix-indicator' is shown
+as a prefix to the current under selection candidate, while the
+remaining of the candidates will receive the indicator controlled
+by `icomplete-vertical-unselected-prefix-indicator'."
+  :type 'boolean
+  :group 'icomplete
+  :version "31.1")
+
+(defcustom icomplete-vertical-selected-prefix-indicator "» "
+  "Prefix string used to mark the selected completion candidate.
+If `icomplete-vertical-render-prefix-indicator' is t, the string
+defined 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-indicator-face'."
+  :type 'string
+  :group 'icomplete
+  :version "31.1")
+
+(defcustom icomplete-vertical-unselected-prefix-indicator "  "
+  "Prefix string used on the unselected completion candidates.
+If `icomplete-vertical-render-prefix-indicator' is t, the string
+defined 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-indicator-face'."
+  :type 'string
+  :group 'icomplete
+  :version "31.1")
 
 ;;;_* Initialization
 
@@ -828,6 +880,60 @@ by `group-function''s second \"transformation\" protocol."
                  else collect (list tr prefix suffix ))
       annotated)))
 
+(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 (current-column))
+             (prefix-indicator-width
+              (if icomplete-vertical-render-prefix-indicator
+                  (max (length icomplete-vertical-selected-prefix-indicator)
+                       (length icomplete-vertical-unselected-prefix-indicator))
+                0))
+             (wrapped-line (with-current-buffer buffer
+                             (save-excursion
+                               (goto-char (car data))
+                               (beginning-of-line)
+                               (count-screen-lines (point) (car data)))))
+             (window-width (+ (window-hscroll) (window-body-width)))
+             (longest-line-width (apply #'max (mapcar #'length lines)))
+             (spaces-to-add
+              (if (> wrapped-line 1)
+                  (- column (* (- wrapped-line 1) (- window-width 5)))
+                column))
+             (spaces-to-add-avoiding-scrolling
+              (if (>= (+ spaces-to-add longest-line-width prefix-indicator-width) window-width)
+                  (- spaces-to-add longest-line-width)
+                spaces-to-add)))
+
+        (dolist (l lines)
+          (add-text-properties
+           0 1 `(display ,(concat (make-string spaces-to-add-avoiding-scrolling ?\s) (substring l 0 1)))
+           l))
+        lines)
+    lines))
+
+(defun icomplete-vertical--ensure-visible-lines-inside-buffer ()
+  "Ensure the completion list is visible in regular buffers only.
+Scrolls the screen to be at least `icomplete-prospects-height' real lines
+away from the bottom.  Counts wrapped lines as real lines."
+  (unless (minibufferp)
+    (let* ((window-height (window-body-height))
+           (current-line (count-screen-lines (window-start) (point)))
+           (lines-to-bottom (- window-height current-line)))
+      (when (< lines-to-bottom icomplete-prospects-height)
+        (scroll-up (- icomplete-prospects-height lines-to-bottom))))))
+
+(defun icomplete-vertical--add-indicator-to-selected (comp)
+  "Add indicators to the selected/unselected COMP completions."
+  (if (and icomplete-vertical-render-prefix-indicator
+           (get-text-property 0 'icomplete-selected comp))
+      (concat (propertize icomplete-vertical-selected-prefix-indicator
+                          'face 'icomplete-vertical-selected-prefix-indicator-face)
+              comp)
+    (concat (propertize icomplete-vertical-unselected-prefix-indicator
+                        'face 'icomplete-vertical-unselected-prefix-indicator-face)
+            comp)))
+
 (cl-defun icomplete--render-vertical
     (comps md &aux scroll-above scroll-below
            (total-space ; number of mini-window lines available
@@ -843,12 +949,17 @@ by `group-function''s second \"transformation\" protocol."
   ;; - 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
+  ;;   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
+             (not icomplete--scrolled-completions)
+             (not icomplete--scrolled-past))
+    (icomplete-vertical--ensure-visible-lines-inside-buffer))
   (when (and icomplete-scroll
              icomplete--scrolled-completions
              (null icomplete--scrolled-past))
+    (icomplete-vertical--ensure-visible-lines-inside-buffer)
     (cl-loop with preds
              for (comp . rest) on comps
              when (equal comp (car icomplete--scrolled-completions))
@@ -903,13 +1014,14 @@ by `group-function''s second \"transformation\" protocol."
      when section
      collect (propertize section 'face 'icomplete-section) into lines-aux
      and count 1 into nsections-aux
+     for comp = (icomplete-vertical--add-indicator-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-prefix-len (length prefix)) ? )
+                     (make-string (max 0 (- max-prefix-len (length prefix))) ? )
                      (completion-lazy-hilit comp)
-                     (make-string (- max-comp-len (length comp)) ? )
+                     (make-string (max 0 (- max-comp-len (length comp))) ? )
                      suffix)
      into lines-aux
      finally (setq lines lines-aux
@@ -924,6 +1036,9 @@ by `group-function''s second \"transformation\" protocol."
                  ((> (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"
-- 
2.48.1

-- 

Rahul Martim Juliato