Subject: bug#76941: [PATCH] completion-preview.el: Provide an API.

From: Eshel Yaron <[email-masked]>
Date: Tue, 11 Mar 2025 21:45:13 +0100

Eli Zaretskii <[email-masked]> writes:

>> From: Eshel Yaron <[email-masked]>
>> Cc: [email-masked]
>> Date: Tue, 11 Mar 2025 15:18:54 +0100
>> 
>> Eli Zaretskii <[email-masked]> writes:
>> 
>> >> Date: Tue, 11 Mar 2025 13:51:50 +0100
>> >> From:  Eshel Yaron via "Bug reports for GNU Emacs,
>> >>  the Swiss army knife of text editors" <[email-masked]>
>> >> 
>> >> This patch adds an initial API for controlling Completion Preview mode's
>> >> preview overlay from Lisp programs, so other features can reuse the
>> >> completion preview UI.
>> >> 
>> >> Any thoughts/comments?
>> >
>> > Should this be documented in the ELisp manual?
>> 
>> Maybe, I can definitely add something if you think we should.  I wonder
>> where to mention it though, perhaps in `(elisp)Completion in Buffers'?
>
> Yes, there.

OK, done in the updated patch below.

>From 8d7985ab4b0de379d38194256c9e29b456796b37 Mon Sep 17 00:00:00 2001
From: Eshel Yaron <[email-masked]>
Date: Tue, 11 Mar 2025 11:16:49 +0100
Subject: [PATCH v2] completion-preview.el: Provide an API.

Add new functions for summoning and dismissing the
completion preview programmatically.  This lets Lisp code
leverage the completion preview UI for different use cases.

* lisp/completion-preview.el (completion-preview-show): New
function.
(completion-preview-hide): Rename to
'completion-preview--hide', use old name for new public API
function.
(completion-preview--hide): New name, see above.
(completion-preview-active-mode, completion-preview--update)
(completion-preview--show)
(completion-preview--window-selection-change)
(completion-preview--post-command, completion-preview-insert)
(completion-preview-partial-insert)
(completion-preview-complete, completion-preview-mode): Use
new functions to show/hide the preview.

* doc/lispref/minibuf.texi (Completion in Buffers): Document
'completion-preview-show' and 'completion-preview-hide'.

* etc/NEWS: Announce new API functions.
---
 doc/lispref/minibuf.texi   | 22 +++++++++
 etc/NEWS                   |  5 ++
 lisp/completion-preview.el | 98 +++++++++++++++++++++++++-------------
 3 files changed, 93 insertions(+), 32 deletions(-)

diff --git a/doc/lispref/minibuf.texi b/doc/lispref/minibuf.texi
index 3cc206d2e1d..3dcab636245 100644
--- a/doc/lispref/minibuf.texi
+++ b/doc/lispref/minibuf.texi
@@ -2237,6 +2237,28 @@ Completion in Buffers
 and @var{end}.
 @end defun
 
+  Emacs can also @emph{suggest} possible in-buffer completions by means
+of a @dfn{completion preview}.  The preview appears after a given
+stretch of buffer text, usually ending at point, and suggest a possible
+completion of that text.  Users can interact with the preview to accept
+the completion suggestion or cycle among multiple suggestions.  Use the
+following functions to create or dismiss a completion preview:
+
+@defun completion-preview-show beg end common suffixes &optional base &rest props
+This function shows a completion suggestion for the text between
+@var{beg} and @var{end} in the current buffer.  @var{common} is a common
+prefix of all completion suggestions; @var{suffixes} is a list of
+distinct suffixes that, together with @var{common}, make up the
+completion suggestions.  The order of @var{suffixes} is the order in
+which the completion preview displays the suggestions, in particular,
+the first element of @var{suffixes} is the one that appears initially.
+@end defun
+
+@defun completion-preview-hide
+This function hides the completion preview, if there is one.
+@end defun
+
+See the commentary in @file{completion-preview.el} for more details.
 
 @node Yes-or-No Queries
 @section Yes-or-No Queries
diff --git a/etc/NEWS b/etc/NEWS
index 926d426eaae..6524f9bdd4e 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -199,6 +199,11 @@ that your popup interface uses for a more integrated experience.
 Note: 'completion-preview-sort-function' was present also in Emacs 30.1,
 albeit as a variable, not a user option.
 
++++
+*** New functions 'completion-preview-show' and 'completion-preview-hide'.
+These new API functions allow Lisp programs to summon and dismiss the
+completion preview at will.
+
 ** Windows
 
 +++
diff --git a/lisp/completion-preview.el b/lisp/completion-preview.el
index 8f7cba580ba..7d263e3678c 100644
--- a/lisp/completion-preview.el
+++ b/lisp/completion-preview.el
@@ -123,6 +123,10 @@
 ;;             (lambda ()
 ;;               (setq-local completion-preview-overlay-priority 1200)
 ;;               (completion-preview-mode)))
+;;
+;; This library also provides an API that you can use to summon and
+;; dismiss the completion preview programmatically; see the functions
+;; `completion-preview-show' and `completion-preview-hide' for details.
 
 ;;; Code:
 
@@ -336,7 +340,7 @@ completion-preview-require-minimum-symbol-length
         (and bounds (<= completion-preview-minimum-symbol-length
                         (- (cdr bounds) (car bounds)))))))
 
-(defun completion-preview-hide ()
+(defun completion-preview--hide ()
   "Hide the completion preview."
   (when completion-preview--overlay
     (delete-overlay completion-preview--overlay)
@@ -418,7 +422,7 @@ completion-preview--window-selection-change
   (unless (or (eq window (selected-window))
               (eq window (minibuffer-selected-window)))
     (with-current-buffer (window-buffer window)
-      (completion-preview-active-mode -1))))
+      (completion-preview-hide))))
 
 (define-minor-mode completion-preview-active-mode
   "Mode for when the completion preview is shown."
@@ -435,7 +439,7 @@ completion-preview-active-mode
               completion-preview-active-mode-map))
     (remove-hook 'window-selection-change-functions
                  #'completion-preview--window-selection-change t)
-    (completion-preview-hide)))
+    (completion-preview--hide)))
 
 (defvar completion-preview-completion-styles '(basic)
   "List of completion styles that Completion Preview mode uses.
@@ -521,28 +525,7 @@ completion-preview--update
       (run-hook-wrapped
        'completion-at-point-functions
        #'completion-preview--capf-wrapper)
-    (when-let* ((suffix (car suffixes)))
-      (set-text-properties 0 (length suffix)
-                           (list 'face (if (cdr suffixes)
-                                           'completion-preview
-                                         'completion-preview-exact))
-                           suffix)
-      (set-text-properties 0 (length common)
-                           (list 'face (if (cdr suffixes)
-                                           'completion-preview-common
-                                         'completion-preview-exact))
-                           common)
-      (let ((ov (completion-preview--make-overlay
-                 end (completion-preview--propertize-for-mouse
-                      (concat (substring common (- end beg)) suffix)))))
-        (overlay-put ov 'completion-preview-beg beg)
-        (overlay-put ov 'completion-preview-end end)
-        (overlay-put ov 'completion-preview-index 0)
-        (overlay-put ov 'completion-preview-suffixes suffixes)
-        (overlay-put ov 'completion-preview-common common)
-        (overlay-put ov 'completion-preview-base base)
-        (overlay-put ov 'completion-preview-props props)
-        (completion-preview-active-mode)))))
+    (apply #'completion-preview-show beg end common suffixes base props)))
 
 (defun completion-preview--try-update ()
   "Try to update completion preview, but give up as soon as input arrives."
@@ -595,7 +578,7 @@ completion-preview--show
                              (substring cand (- end beg))))
                        'completion-preview-end end)
         ;; The previous preview is no longer applicable, hide it.
-        (completion-preview-active-mode -1))))
+        (completion-preview-hide))))
   ;; Run `completion-at-point-functions' to get a new candidate.
   (if completion-preview-idle-delay
       (setq completion-preview--timer
@@ -622,7 +605,7 @@ completion-preview--post-command
                (completion-preview-require-minimum-symbol-length)
                (not buffer-read-only))
           (completion-preview--show)
-        (completion-preview-active-mode -1)))))
+        (completion-preview-hide)))))
 
 (defun completion-preview--barf-if-no-preview ()
   "Signal a `user-error' if completion preview is not active."
@@ -642,7 +625,7 @@ completion-preview-insert
                          :exit-function))
          (aft (completion-preview--get 'after-string))
          (str (concat pre com (nth ind all))))
-    (completion-preview-active-mode -1)
+    (completion-preview-hide)
     (goto-char end)
     (insert-and-inherit (substring-no-properties aft))
     (when (functionp efn) (funcall efn str 'finished))))
@@ -691,7 +674,7 @@ completion-preview-partial-insert
              (efn (plist-get
                    (completion-preview--get 'completion-preview-props)
                    :exit-function)))
-        (completion-preview-active-mode -1)
+        (completion-preview-hide)
         (when (functionp efn) (funcall efn (concat pre com (nth ind all))
                                        'finished))))
      ;; If we kept anything, update preview overlay accordingly.
@@ -756,7 +739,7 @@ completion-preview-complete
                (props (plist-put props :exit-function
                                  (when (functionp efn)
                                    (lambda (string status)
-                                     (completion-preview-active-mode -1)
+                                     (completion-preview-hide)
                                      (funcall efn string status)))))
                ;; The predicate is meant for the original completion
                ;; candidates, which may be symbols or cons cells, but
@@ -778,7 +761,7 @@ completion-preview-complete
               ;; Remove stale preview since `efn' can make arbitrary
               ;; text and point modifications that might interfere with
               ;; a subsequent preview update.  See bug#76606.
-              (completion-preview-active-mode -1)
+              (completion-preview-hide)
               (funcall efn (concat base com) (if (cdr all) 'exact 'finished)))
           ;; Otherwise, remove the common prefix from the preview.
           (completion-preview--inhibit-update)
@@ -861,7 +844,7 @@ completion-preview-mode
   (if completion-preview-mode
       (add-hook 'post-command-hook #'completion-preview--post-command nil t)
     (remove-hook 'post-command-hook #'completion-preview--post-command t)
-    (when completion-preview-active-mode (completion-preview-active-mode -1))
+    (when completion-preview-active-mode (completion-preview-hide))
     (when (timerp completion-preview--timer)
       (cancel-timer completion-preview--timer)
       (setq completion-preview--timer nil))))
@@ -882,5 +865,56 @@ global-completion-preview-mode
                     wdired-mode)
                t))
 
+
+;;; API
+
+(defun completion-preview-show (beg end common suffixes &optional base &rest props)
+  "Display an inline completion preview.
+
+BEG and END are buffer positions, the beginning and end of the buffer
+text for which the preview shows a completion suggestion, respectively.
+The completion preview is displayed right after END.
+
+COMMON is a common prefix of all available completion candidates.
+SUFFIXES is a list of strings such that prepending COMMON to a suffix
+yields a completion candidate.
+
+The completion preview initially displays the first completion
+candidate, the candidate obtained by prepending COMMON to the first
+element of SUFFIXES.
+
+PROPS is a list of extra metadata, like `completion-extra-properties'.
+Currently the only meaningful property is `:exit-function'.
+
+Optional argument BASE, if non-nil, is an additional prefix to prepend
+to a completion suggestion you accept before reporting it to the given
+`:exit-function'.  BASE does not influence completion."
+  (when-let* ((suffix (car suffixes)))
+    (set-text-properties 0 (length suffix)
+                         (list 'face (if (cdr suffixes)
+                                         'completion-preview
+                                       'completion-preview-exact))
+                         suffix)
+    (set-text-properties 0 (length common)
+                         (list 'face (if (cdr suffixes)
+                                         'completion-preview-common
+                                       'completion-preview-exact))
+                         common)
+    (let ((ov (completion-preview--make-overlay
+               end (completion-preview--propertize-for-mouse
+                    (concat (substring common (- end beg)) suffix)))))
+      (overlay-put ov 'completion-preview-beg beg)
+      (overlay-put ov 'completion-preview-end end)
+      (overlay-put ov 'completion-preview-index 0)
+      (overlay-put ov 'completion-preview-suffixes suffixes)
+      (overlay-put ov 'completion-preview-common (or common ""))
+      (overlay-put ov 'completion-preview-base (or base ""))
+      (overlay-put ov 'completion-preview-props props)
+      (completion-preview-active-mode))))
+
+(defun completion-preview-hide ()
+  "Hide the completion preview and reset its state."
+  (completion-preview-active-mode -1))
+
 (provide 'completion-preview)
 ;;; completion-preview.el ends here
-- 
2.48.1