GNU bug report logs - #67275
[PATCH] ; Improve and add tests for Completion Preview mode

Please note: This is a static page, with minimal formatting, updated once a day.
Click here to see this page with the latest information and nicer formatting.

Package: emacs; Reported by: Eshel Yaron <me@HIDDEN>; Keywords: patch; dated Sun, 19 Nov 2023 10:27:02 UTC; Maintainer for emacs is bug-gnu-emacs@HIDDEN.

Message received at submit <at> debbugs.gnu.org:


Received: (at submit) by debbugs.gnu.org; 20 Nov 2023 12:27:04 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Nov 20 07:27:04 2023
Received: from localhost ([127.0.0.1]:52676 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1r53Ml-0005Rh-Ss
	for submit <at> debbugs.gnu.org; Mon, 20 Nov 2023 07:27:04 -0500
Received: from lists.gnu.org ([2001:470:142::17]:43672)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <me@HIDDEN>) id 1r53Mi-0005R3-Ho
 for submit <at> debbugs.gnu.org; Mon, 20 Nov 2023 07:27:02 -0500
Received: from eggs.gnu.org ([2001:470:142:3::10])
 by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <me@HIDDEN>) id 1r53MZ-0007yv-SV
 for bug-gnu-emacs@HIDDEN; Mon, 20 Nov 2023 07:26:52 -0500
Received: from mail.eshelyaron.com ([107.175.124.16] helo=eshelyaron.com)
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <me@HIDDEN>)
 id 1r53MX-0007DO-GS; Mon, 20 Nov 2023 07:26:51 -0500
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=eshelyaron.com;
 s=mail; t=1700483207;
 bh=iCybZHWe2hoEH0JNLzf8g3XiiGnaP4WG67bH6+AooIs=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=tizDEgapTMC8QGdKQlu+fZIGnAWnZVlkqPJi+TLbBjCiguzA1TySxlNvxAfxT+3d4
 Q9FydBg1irLouCbOAaPjKZZFJbPcosR8PfD3lBDCKZJm6c8hCExNd80gN3P8vda/XP
 lfeUJm0z5tW/9MhrTbF9C6B5RsMuXf/OxYdJDalYiSACkJAUWUB9gQmYAY8l1QYSBO
 B97mtEDWCFMahKf3G44ZCKTQFN8lpC/Zl3nZMOHw95tp3O3iljQ+5emC6dEO4KDYlN
 ygkycTc5BzAOjdw1VD/ui5/hWrLQm1hRVBaKJfLqu/RimMytH/aM7zOQG2xOKf/jxn
 UkVo4MZEuwFGA==
From: Eshel Yaron <me@HIDDEN>
To: Eshel Yaron via "Bug reports for GNU Emacs, the Swiss army knife of text
 editors" <bug-gnu-emacs@HIDDEN>
Subject: Re: bug#67275: [PATCH] ; Improve and add tests for Completion
 Preview mode
In-Reply-To: <m1jzqe9ecb.fsf@HIDDEN> (Eshel Yaron via's message of
 "Sun, 19 Nov 2023 12:23:16 +0100")
References: <m1sf529gzw.fsf@HIDDEN> <8334x2ko1y.fsf@HIDDEN>
 <m1jzqe9ecb.fsf@HIDDEN>
Date: Mon, 20 Nov 2023 13:26:45 +0100
Message-ID: <m1r0kkwqyi.fsf@HIDDEN>
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="=-=-="
Received-SPF: pass client-ip=107.175.124.16; envelope-from=me@HIDDEN;
 helo=eshelyaron.com
X-Spam_score_int: -20
X-Spam_score: -2.1
X-Spam_bar: --
X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1,
 DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, SPF_HELO_PASS=-0.001,
 SPF_PASS=-0.001, T_SCC_BODY_TEXT_LINE=-0.01 autolearn=ham autolearn_force=no
X-Spam_action: no action
X-Spam-Score: 0.9 (/)
X-Debbugs-Envelope-To: submit
Cc: Eli Zaretskii <eliz@HIDDEN>, 67275 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.1 (/)

--=-=-=
Content-Type: text/plain

Eshel Yaron writes:

> ...Here's the updated patch...

I'm attaching below another patch, which is meant to be applied on top
of the patch from my previous message.

This patch simplifies Completion Preview mode a bit and, crucially,
fixes an issue where certain `completion-at-point-functions`
configurations could prevent `completion-preview-insert` from actually
inserting the completion suggestion.

I could unify this patch with the previous one and submit them as a
single patch if that'd be preferable, although in terms of Git history
it might be nicer to commit them separately IMO.


Thanks,

Eshel


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment;
 filename=0001-Avoid-completion-at-point-in-completion-preview-inse.patch

From f9a5099971511ce1b4298363395fd624453fb70d Mon Sep 17 00:00:00 2001
From: Eshel Yaron <me@HIDDEN>
Date: Mon, 20 Nov 2023 12:45:11 +0100
Subject: [PATCH] ; Avoid 'completion-at-point' in 'completion-preview-insert'

Insert the completion suggestion directly in
'completion-preview-insert' instead of using 'completion-at-point' to
do that.  This fixes an issue where 'completion-preview-insert' would
not work correctly when the user uses 'add-hook' with a DEPTH argument
below a certain value to add functions to
'completion-at-point-functions', and obviates the need to manipulate
'completion-at-point-functions' when showing the preview all together.

* lisp/completion-preview.el (completion-preview--make-overlay)
(completion-preview-prev-candidate)
(completion-preview-next-candidate)
(completion-preview-mode): Improve docstring.
(completion-preview--exit-function)
(completion-preview--insert)
(completion-preview-insert-on-completion): Remove, no longer used.
(completion-preview--get): Turn into a 'defsubst'.
(completion-preview-active-mode)
(completion-preview--capf-wrapper): Simplify.
(completion-preview--try-table)
(completion-preview--update): Keep the completion "base" as a property
of the preview overlay, for use in completion exit functions.
(completion-preview-insert): Insert completion and call exit function
directly instead of manipulating 'completion-at-point' to do so.
---
 lisp/completion-preview.el | 121 ++++++++++++++++---------------------
 1 file changed, 52 insertions(+), 69 deletions(-)

diff --git a/lisp/completion-preview.el b/lisp/completion-preview.el
index 95410e2e5cd..039a330bc84 100644
--- a/lisp/completion-preview.el
+++ b/lisp/completion-preview.el
@@ -22,10 +22,11 @@
 ;;; Commentary:
 
 ;; This library provides the Completion Preview mode.  This minor mode
-;; displays the top completion candidate for the symbol at point in an
+;; displays a completion suggestion for the symbol at point in an
 ;; overlay after point.  Check out the customization group
 ;; `completion-preview' for user options that you may want to tweak.
 ;;
+;; To enable Completion Preview mode, use `completion-preview-mode'.
 ;; To accept the completion suggestion, press TAB.  If you want to
 ;; ignore a completion suggestion, just go on editing or moving around
 ;; the buffer.  Completion Preview mode continues to update the
@@ -48,15 +49,6 @@
 ;; that should appear around point for Emacs to suggest a completion.
 ;; By default, this option is set to 3, so Emacs suggests a completion
 ;; if you type "foo", but typing just "fo" doesn't show the preview.
-;;
-;; The user option `completion-preview-insert-on-completion' controls
-;; what happens when you invoke `completion-at-point' while the
-;; completion preview is visible.  By default this option is nil,
-;; which tells `completion-at-point' to ignore the completion preview
-;; and show the list of completion candidates as usual.  If you set
-;; `completion-preview-insert-on-completion' to non-nil, then
-;; `completion-at-point' inserts the preview directly without looking
-;; for more candidates.
 
 ;;; Code:
 
@@ -91,11 +83,6 @@ completion-preview-minimum-symbol-length
   :type 'natnum
   :version "30.1")
 
-(defcustom completion-preview-insert-on-completion nil
-  "Whether \\[completion-at-point] inserts the previewed suggestion."
-  :type 'boolean
-  :version "30.1")
-
 (defvar completion-preview-sort-function #'minibuffer--sort-by-length-alpha
   "Sort function to use for choosing a completion candidate to preview.")
 
@@ -149,7 +136,7 @@ completion-preview-hide
     (setq completion-preview--overlay nil)))
 
 (defun completion-preview--make-overlay (pos string)
-  "Make a new completion preview overlay at POS showing STRING."
+  "Make preview overlay showing STRING at POS, or move existing preview there."
   (if completion-preview--overlay
       (move-overlay completion-preview--overlay pos pos)
     (setq completion-preview--overlay (make-overlay pos pos))
@@ -162,23 +149,14 @@ completion-preview--make-overlay
       (overlay-put completion-preview--overlay 'after-string string))
     completion-preview--overlay))
 
-(defun completion-preview--get (prop)
+(defsubst completion-preview--get (prop)
   "Return property PROP of the completion preview overlay."
   (overlay-get completion-preview--overlay prop))
 
 (define-minor-mode completion-preview-active-mode
   "Mode for when the completion preview is shown."
   :interactive nil
-  (if completion-preview-active-mode
-      (add-hook 'completion-at-point-functions #'completion-preview--insert -1 t)
-    (remove-hook 'completion-at-point-functions #'completion-preview--insert t)
-    (completion-preview-hide)))
-
-(defun completion-preview--exit-function (func)
-  "Return an exit function that hides the completion preview and calls FUNC."
-  (lambda (&rest args)
-    (completion-preview-active-mode -1)
-    (when (functionp func) (apply func args))))
+  (unless completion-preview-active-mode (completion-preview-hide)))
 
 (defun completion-preview--try-table (table beg end props)
   "Check TABLE for a completion matching the text between BEG and END.
@@ -187,16 +165,16 @@ completion-preview--try-table
 See `completion-at-point-functions' for more details.
 
 If TABLE contains a matching completion, return a list
-\(PREVIEW BEG END ALL EXIT-FN) where PREVIEW is the text to show
-in the completion preview, ALL is the list of all matching
-completion candidates, and EXIT-FN is either a function to call
-after inserting PREVIEW or nil.  If TABLE does not contain
-matching completions, or if there are multiple matching
-completions and `completion-preview-exact-match-only' is non-nil,
-return nil instead."
+\(PREVIEW BEG END ALL BASE EXIT-FN) where PREVIEW is the text to
+show in the completion preview, ALL is the list of all matching
+completion candidates, BASE is a common prefix that TABLE elided
+from the start of each candidate, and EXIT-FN is either a
+function to call after inserting PREVIEW or nil.  If TABLE does
+not contain matching completions, or if there are multiple
+matching completions and `completion-preview-exact-match-only' is
+non-nil, return nil instead."
   (let* ((pred (plist-get props :predicate))
-         (exit-fn (completion-preview--exit-function
-                   (plist-get props :exit-function)))
+         (exit-fn (plist-get props :exit-function))
          (string (buffer-substring beg end))
          (md (completion-metadata string table pred))
          (sort-fn (or (completion-metadata-get md 'cycle-sort-function)
@@ -217,23 +195,23 @@ completion-preview--try-table
                             'face (if (cdr sorted)
                                       'completion-preview
                                     'completion-preview-exact))
-                (+ beg base) end sorted exit-fn))))))
+                (+ beg base) end sorted
+                (substring string 0 base) exit-fn))))))
 
 (defun completion-preview--capf-wrapper (capf)
   "Translate return value of CAPF to properties for completion preview overlay."
-  (unless (eq capf #'completion-preview--insert)
-    (let ((res (ignore-errors (funcall capf))))
-      (and (consp res)
-           (not (functionp res))
-           (seq-let (beg end table &rest plist) res
-             (or (completion-preview--try-table table beg end plist)
-                 (unless (eq 'no (plist-get plist :exclusive))
-                   ;; Return non-nil to exclude other capfs.
-                   '(nil))))))))
+  (let ((res (ignore-errors (funcall capf))))
+    (and (consp res)
+         (not (functionp res))
+         (seq-let (beg end table &rest plist) res
+           (or (completion-preview--try-table table beg end plist)
+               (unless (eq 'no (plist-get plist :exclusive))
+                 ;; Return non-nil to exclude other capfs.
+                 '(nil)))))))
 
 (defun completion-preview--update ()
   "Update completion preview."
-  (seq-let (preview beg end all exit-fn)
+  (seq-let (preview beg end all base exit-fn)
       (run-hook-wrapped
        'completion-at-point-functions
        #'completion-preview--capf-wrapper)
@@ -243,6 +221,7 @@ completion-preview--update
         (overlay-put ov 'completion-preview-end end)
         (overlay-put ov 'completion-preview-index 0)
         (overlay-put ov 'completion-preview-cands all)
+        (overlay-put ov 'completion-preview-base base)
         (overlay-put ov 'completion-preview-exit-fn exit-fn)
         (completion-preview-active-mode)))))
 
@@ -296,35 +275,30 @@ completion-preview--post-command
        (completion-preview--show))
     (completion-preview-active-mode -1)))
 
-(defun completion-preview--insert ()
-  "Completion at point function for inserting the current preview.
-
-When `completion-preview-insert-on-completion' is nil, this
-function returns nil.  Completion Preview mode adds this function
-to `completion-at-point-functions' when the preview is shown,
-such that `completion-at-point' inserts the preview candidate if
-and only if `completion-preview-insert-on-completion' is non-nil."
-  (when (and completion-preview-active-mode
-             completion-preview-insert-on-completion)
-    (list (completion-preview--get 'completion-preview-beg)
-          (completion-preview--get 'completion-preview-end)
-          (list (nth (completion-preview--get 'completion-preview-index)
-                     (completion-preview--get 'completion-preview-cands)))
-          :exit-function (completion-preview--get 'completion-preview-exit-fn))))
-
 (defun completion-preview-insert ()
-  "Insert the completion candidate that the preview shows."
+  "Insert the completion candidate that the preview is showing."
   (interactive)
-  (let ((completion-preview-insert-on-completion t))
-    (completion-at-point)))
+  (if completion-preview-active-mode
+      (let* ((pre (completion-preview--get 'completion-preview-base))
+             (end (completion-preview--get 'completion-preview-end))
+             (ind (completion-preview--get 'completion-preview-index))
+             (all (completion-preview--get 'completion-preview-cands))
+             (efn (completion-preview--get 'completion-preview-exit-fn))
+             (aft (completion-preview--get 'after-string))
+             (str (concat pre (nth ind all))))
+        (completion-preview-active-mode -1)
+        (goto-char end)
+        (insert (substring-no-properties aft))
+        (when (functionp efn) (funcall efn str 'finished)))
+    (user-error "No current completion preview")))
 
 (defun completion-preview-prev-candidate ()
-  "Cycle the candidate that the preview shows to the previous suggestion."
+  "Cycle the candidate that the preview is showing to the previous suggestion."
   (interactive)
   (completion-preview-next-candidate -1))
 
 (defun completion-preview-next-candidate (direction)
-  "Cycle the candidate that the preview shows in direction DIRECTION.
+  "Cycle the candidate that the preview is showing in direction DIRECTION.
 
 DIRECTION should be either 1 which means cycle forward, or -1
 which means cycle backward.  Interactively, DIRECTION is the
@@ -351,7 +325,16 @@ completion-preview-next-candidate
 
 ;;;###autoload
 (define-minor-mode completion-preview-mode
-  "Show in-buffer completion preview as you type."
+  "Show in-buffer completion suggestions in a preview as you type.
+
+This mode automatically shows and updates the completion preview
+according to the text around point.
+\\<completion-preview-active-mode-map>\
+When the preview is visible, \\[completion-preview-insert]
+accepts the completion suggestion,
+\\[completion-preview-next-candidate] cycles forward to the next
+completion suggestion, and \\[completion-preview-prev-candidate]
+cycles backward."
   :lighter " CP"
   (if completion-preview-mode
       (add-hook 'post-command-hook #'completion-preview--post-command nil t)
-- 
2.42.0


--=-=-=--




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#67275; Package emacs. Full text available.

Message received at 67275 <at> debbugs.gnu.org:


Received: (at 67275) by debbugs.gnu.org; 20 Nov 2023 12:26:53 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Nov 20 07:26:53 2023
Received: from localhost ([127.0.0.1]:52672 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1r53Ma-0005Qt-WB
	for submit <at> debbugs.gnu.org; Mon, 20 Nov 2023 07:26:53 -0500
Received: from mail.eshelyaron.com ([107.175.124.16]:41028 helo=eshelyaron.com)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <me@HIDDEN>) id 1r53MY-0005Ql-FL
 for 67275 <at> debbugs.gnu.org; Mon, 20 Nov 2023 07:26:52 -0500
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=eshelyaron.com;
 s=mail; t=1700483207;
 bh=iCybZHWe2hoEH0JNLzf8g3XiiGnaP4WG67bH6+AooIs=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=tizDEgapTMC8QGdKQlu+fZIGnAWnZVlkqPJi+TLbBjCiguzA1TySxlNvxAfxT+3d4
 Q9FydBg1irLouCbOAaPjKZZFJbPcosR8PfD3lBDCKZJm6c8hCExNd80gN3P8vda/XP
 lfeUJm0z5tW/9MhrTbF9C6B5RsMuXf/OxYdJDalYiSACkJAUWUB9gQmYAY8l1QYSBO
 B97mtEDWCFMahKf3G44ZCKTQFN8lpC/Zl3nZMOHw95tp3O3iljQ+5emC6dEO4KDYlN
 ygkycTc5BzAOjdw1VD/ui5/hWrLQm1hRVBaKJfLqu/RimMytH/aM7zOQG2xOKf/jxn
 UkVo4MZEuwFGA==
From: Eshel Yaron <me@HIDDEN>
To: Eshel Yaron via "Bug reports for GNU Emacs, the Swiss army knife of text
 editors" <bug-gnu-emacs@HIDDEN>
Subject: Re: bug#67275: [PATCH] ; Improve and add tests for Completion
 Preview mode
In-Reply-To: <m1jzqe9ecb.fsf@HIDDEN> (Eshel Yaron via's message of
 "Sun, 19 Nov 2023 12:23:16 +0100")
References: <m1sf529gzw.fsf@HIDDEN> <8334x2ko1y.fsf@HIDDEN>
 <m1jzqe9ecb.fsf@HIDDEN>
Date: Mon, 20 Nov 2023 13:26:45 +0100
Message-ID: <m1r0kkwqyi.fsf@HIDDEN>
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="=-=-="
X-Spam-Score: -0.0 (/)
X-Debbugs-Envelope-To: 67275
Cc: Eli Zaretskii <eliz@HIDDEN>, 67275 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)

--=-=-=
Content-Type: text/plain

Eshel Yaron writes:

> ...Here's the updated patch...

I'm attaching below another patch, which is meant to be applied on top
of the patch from my previous message.

This patch simplifies Completion Preview mode a bit and, crucially,
fixes an issue where certain `completion-at-point-functions`
configurations could prevent `completion-preview-insert` from actually
inserting the completion suggestion.

I could unify this patch with the previous one and submit them as a
single patch if that'd be preferable, although in terms of Git history
it might be nicer to commit them separately IMO.


Thanks,

Eshel


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment;
 filename=0001-Avoid-completion-at-point-in-completion-preview-inse.patch

From f9a5099971511ce1b4298363395fd624453fb70d Mon Sep 17 00:00:00 2001
From: Eshel Yaron <me@HIDDEN>
Date: Mon, 20 Nov 2023 12:45:11 +0100
Subject: [PATCH] ; Avoid 'completion-at-point' in 'completion-preview-insert'

Insert the completion suggestion directly in
'completion-preview-insert' instead of using 'completion-at-point' to
do that.  This fixes an issue where 'completion-preview-insert' would
not work correctly when the user uses 'add-hook' with a DEPTH argument
below a certain value to add functions to
'completion-at-point-functions', and obviates the need to manipulate
'completion-at-point-functions' when showing the preview all together.

* lisp/completion-preview.el (completion-preview--make-overlay)
(completion-preview-prev-candidate)
(completion-preview-next-candidate)
(completion-preview-mode): Improve docstring.
(completion-preview--exit-function)
(completion-preview--insert)
(completion-preview-insert-on-completion): Remove, no longer used.
(completion-preview--get): Turn into a 'defsubst'.
(completion-preview-active-mode)
(completion-preview--capf-wrapper): Simplify.
(completion-preview--try-table)
(completion-preview--update): Keep the completion "base" as a property
of the preview overlay, for use in completion exit functions.
(completion-preview-insert): Insert completion and call exit function
directly instead of manipulating 'completion-at-point' to do so.
---
 lisp/completion-preview.el | 121 ++++++++++++++++---------------------
 1 file changed, 52 insertions(+), 69 deletions(-)

diff --git a/lisp/completion-preview.el b/lisp/completion-preview.el
index 95410e2e5cd..039a330bc84 100644
--- a/lisp/completion-preview.el
+++ b/lisp/completion-preview.el
@@ -22,10 +22,11 @@
 ;;; Commentary:
 
 ;; This library provides the Completion Preview mode.  This minor mode
-;; displays the top completion candidate for the symbol at point in an
+;; displays a completion suggestion for the symbol at point in an
 ;; overlay after point.  Check out the customization group
 ;; `completion-preview' for user options that you may want to tweak.
 ;;
+;; To enable Completion Preview mode, use `completion-preview-mode'.
 ;; To accept the completion suggestion, press TAB.  If you want to
 ;; ignore a completion suggestion, just go on editing or moving around
 ;; the buffer.  Completion Preview mode continues to update the
@@ -48,15 +49,6 @@
 ;; that should appear around point for Emacs to suggest a completion.
 ;; By default, this option is set to 3, so Emacs suggests a completion
 ;; if you type "foo", but typing just "fo" doesn't show the preview.
-;;
-;; The user option `completion-preview-insert-on-completion' controls
-;; what happens when you invoke `completion-at-point' while the
-;; completion preview is visible.  By default this option is nil,
-;; which tells `completion-at-point' to ignore the completion preview
-;; and show the list of completion candidates as usual.  If you set
-;; `completion-preview-insert-on-completion' to non-nil, then
-;; `completion-at-point' inserts the preview directly without looking
-;; for more candidates.
 
 ;;; Code:
 
@@ -91,11 +83,6 @@ completion-preview-minimum-symbol-length
   :type 'natnum
   :version "30.1")
 
-(defcustom completion-preview-insert-on-completion nil
-  "Whether \\[completion-at-point] inserts the previewed suggestion."
-  :type 'boolean
-  :version "30.1")
-
 (defvar completion-preview-sort-function #'minibuffer--sort-by-length-alpha
   "Sort function to use for choosing a completion candidate to preview.")
 
@@ -149,7 +136,7 @@ completion-preview-hide
     (setq completion-preview--overlay nil)))
 
 (defun completion-preview--make-overlay (pos string)
-  "Make a new completion preview overlay at POS showing STRING."
+  "Make preview overlay showing STRING at POS, or move existing preview there."
   (if completion-preview--overlay
       (move-overlay completion-preview--overlay pos pos)
     (setq completion-preview--overlay (make-overlay pos pos))
@@ -162,23 +149,14 @@ completion-preview--make-overlay
       (overlay-put completion-preview--overlay 'after-string string))
     completion-preview--overlay))
 
-(defun completion-preview--get (prop)
+(defsubst completion-preview--get (prop)
   "Return property PROP of the completion preview overlay."
   (overlay-get completion-preview--overlay prop))
 
 (define-minor-mode completion-preview-active-mode
   "Mode for when the completion preview is shown."
   :interactive nil
-  (if completion-preview-active-mode
-      (add-hook 'completion-at-point-functions #'completion-preview--insert -1 t)
-    (remove-hook 'completion-at-point-functions #'completion-preview--insert t)
-    (completion-preview-hide)))
-
-(defun completion-preview--exit-function (func)
-  "Return an exit function that hides the completion preview and calls FUNC."
-  (lambda (&rest args)
-    (completion-preview-active-mode -1)
-    (when (functionp func) (apply func args))))
+  (unless completion-preview-active-mode (completion-preview-hide)))
 
 (defun completion-preview--try-table (table beg end props)
   "Check TABLE for a completion matching the text between BEG and END.
@@ -187,16 +165,16 @@ completion-preview--try-table
 See `completion-at-point-functions' for more details.
 
 If TABLE contains a matching completion, return a list
-\(PREVIEW BEG END ALL EXIT-FN) where PREVIEW is the text to show
-in the completion preview, ALL is the list of all matching
-completion candidates, and EXIT-FN is either a function to call
-after inserting PREVIEW or nil.  If TABLE does not contain
-matching completions, or if there are multiple matching
-completions and `completion-preview-exact-match-only' is non-nil,
-return nil instead."
+\(PREVIEW BEG END ALL BASE EXIT-FN) where PREVIEW is the text to
+show in the completion preview, ALL is the list of all matching
+completion candidates, BASE is a common prefix that TABLE elided
+from the start of each candidate, and EXIT-FN is either a
+function to call after inserting PREVIEW or nil.  If TABLE does
+not contain matching completions, or if there are multiple
+matching completions and `completion-preview-exact-match-only' is
+non-nil, return nil instead."
   (let* ((pred (plist-get props :predicate))
-         (exit-fn (completion-preview--exit-function
-                   (plist-get props :exit-function)))
+         (exit-fn (plist-get props :exit-function))
          (string (buffer-substring beg end))
          (md (completion-metadata string table pred))
          (sort-fn (or (completion-metadata-get md 'cycle-sort-function)
@@ -217,23 +195,23 @@ completion-preview--try-table
                             'face (if (cdr sorted)
                                       'completion-preview
                                     'completion-preview-exact))
-                (+ beg base) end sorted exit-fn))))))
+                (+ beg base) end sorted
+                (substring string 0 base) exit-fn))))))
 
 (defun completion-preview--capf-wrapper (capf)
   "Translate return value of CAPF to properties for completion preview overlay."
-  (unless (eq capf #'completion-preview--insert)
-    (let ((res (ignore-errors (funcall capf))))
-      (and (consp res)
-           (not (functionp res))
-           (seq-let (beg end table &rest plist) res
-             (or (completion-preview--try-table table beg end plist)
-                 (unless (eq 'no (plist-get plist :exclusive))
-                   ;; Return non-nil to exclude other capfs.
-                   '(nil))))))))
+  (let ((res (ignore-errors (funcall capf))))
+    (and (consp res)
+         (not (functionp res))
+         (seq-let (beg end table &rest plist) res
+           (or (completion-preview--try-table table beg end plist)
+               (unless (eq 'no (plist-get plist :exclusive))
+                 ;; Return non-nil to exclude other capfs.
+                 '(nil)))))))
 
 (defun completion-preview--update ()
   "Update completion preview."
-  (seq-let (preview beg end all exit-fn)
+  (seq-let (preview beg end all base exit-fn)
       (run-hook-wrapped
        'completion-at-point-functions
        #'completion-preview--capf-wrapper)
@@ -243,6 +221,7 @@ completion-preview--update
         (overlay-put ov 'completion-preview-end end)
         (overlay-put ov 'completion-preview-index 0)
         (overlay-put ov 'completion-preview-cands all)
+        (overlay-put ov 'completion-preview-base base)
         (overlay-put ov 'completion-preview-exit-fn exit-fn)
         (completion-preview-active-mode)))))
 
@@ -296,35 +275,30 @@ completion-preview--post-command
        (completion-preview--show))
     (completion-preview-active-mode -1)))
 
-(defun completion-preview--insert ()
-  "Completion at point function for inserting the current preview.
-
-When `completion-preview-insert-on-completion' is nil, this
-function returns nil.  Completion Preview mode adds this function
-to `completion-at-point-functions' when the preview is shown,
-such that `completion-at-point' inserts the preview candidate if
-and only if `completion-preview-insert-on-completion' is non-nil."
-  (when (and completion-preview-active-mode
-             completion-preview-insert-on-completion)
-    (list (completion-preview--get 'completion-preview-beg)
-          (completion-preview--get 'completion-preview-end)
-          (list (nth (completion-preview--get 'completion-preview-index)
-                     (completion-preview--get 'completion-preview-cands)))
-          :exit-function (completion-preview--get 'completion-preview-exit-fn))))
-
 (defun completion-preview-insert ()
-  "Insert the completion candidate that the preview shows."
+  "Insert the completion candidate that the preview is showing."
   (interactive)
-  (let ((completion-preview-insert-on-completion t))
-    (completion-at-point)))
+  (if completion-preview-active-mode
+      (let* ((pre (completion-preview--get 'completion-preview-base))
+             (end (completion-preview--get 'completion-preview-end))
+             (ind (completion-preview--get 'completion-preview-index))
+             (all (completion-preview--get 'completion-preview-cands))
+             (efn (completion-preview--get 'completion-preview-exit-fn))
+             (aft (completion-preview--get 'after-string))
+             (str (concat pre (nth ind all))))
+        (completion-preview-active-mode -1)
+        (goto-char end)
+        (insert (substring-no-properties aft))
+        (when (functionp efn) (funcall efn str 'finished)))
+    (user-error "No current completion preview")))
 
 (defun completion-preview-prev-candidate ()
-  "Cycle the candidate that the preview shows to the previous suggestion."
+  "Cycle the candidate that the preview is showing to the previous suggestion."
   (interactive)
   (completion-preview-next-candidate -1))
 
 (defun completion-preview-next-candidate (direction)
-  "Cycle the candidate that the preview shows in direction DIRECTION.
+  "Cycle the candidate that the preview is showing in direction DIRECTION.
 
 DIRECTION should be either 1 which means cycle forward, or -1
 which means cycle backward.  Interactively, DIRECTION is the
@@ -351,7 +325,16 @@ completion-preview-next-candidate
 
 ;;;###autoload
 (define-minor-mode completion-preview-mode
-  "Show in-buffer completion preview as you type."
+  "Show in-buffer completion suggestions in a preview as you type.
+
+This mode automatically shows and updates the completion preview
+according to the text around point.
+\\<completion-preview-active-mode-map>\
+When the preview is visible, \\[completion-preview-insert]
+accepts the completion suggestion,
+\\[completion-preview-next-candidate] cycles forward to the next
+completion suggestion, and \\[completion-preview-prev-candidate]
+cycles backward."
   :lighter " CP"
   (if completion-preview-mode
       (add-hook 'post-command-hook #'completion-preview--post-command nil t)
-- 
2.42.0


--=-=-=--




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#67275; Package emacs. Full text available.

Message received at 67275 <at> debbugs.gnu.org:


Received: (at 67275) by debbugs.gnu.org; 19 Nov 2023 11:23:24 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sun Nov 19 06:23:24 2023
Received: from localhost ([127.0.0.1]:50112 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1r4ftb-0008Pd-7l
	for submit <at> debbugs.gnu.org; Sun, 19 Nov 2023 06:23:24 -0500
Received: from mail.eshelyaron.com ([107.175.124.16]:48620 helo=eshelyaron.com)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <me@HIDDEN>) id 1r4ftY-0008PS-AM
 for 67275 <at> debbugs.gnu.org; Sun, 19 Nov 2023 06:23:21 -0500
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=eshelyaron.com;
 s=mail; t=1700392998;
 bh=J781RpvRBDfQASgwez12CyIH9fSS/ABj6LMgLdLnCZs=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=tavKk+IHvoXc9s1CZBoBdR++N16mx70orhzawhcAaJLXaokoXgSj7/O+AQxdxb4AC
 kOhboiV96hfOBcxv7H80IoX3/ROf3TAFDRa2wAzVV7bpyw78ZAUWuoyZBwzaPGYR1b
 ECWL5R9A7wOKoquSuioYJZT7X2z4OnCuPn0YmYP1khzpLEpSqd9cIjgXHkQ6F5IrHU
 +Xga48g6BGHUagGrX1ooBMYMJsUs5tjAUAD0YlwC+826yfBydFdW+eoLO7HLGFpEqp
 y1CrbVbkW+P0ehmMyXwiCKfoN3Er4PoubndmpRFVbR5Xb6V2pudD1aUX4fvz6vit0l
 CNOGe+WiyXCIg==
From: Eshel Yaron <me@HIDDEN>
To: Eli Zaretskii <eliz@HIDDEN>
Subject: Re: bug#67275: [PATCH] ; Improve and add tests for Completion
 Preview mode
In-Reply-To: <8334x2ko1y.fsf@HIDDEN> (Eli Zaretskii's message of "Sun, 19 Nov
 2023 12:58:01 +0200")
References: <m1sf529gzw.fsf@HIDDEN> <8334x2ko1y.fsf@HIDDEN>
Date: Sun, 19 Nov 2023 12:23:16 +0100
Message-ID: <m1jzqe9ecb.fsf@HIDDEN>
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="=-=-="
X-Spam-Score: -0.0 (/)
X-Debbugs-Envelope-To: 67275
Cc: 67275 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)

--=-=-=
Content-Type: text/plain

Eli Zaretskii <eliz@HIDDEN> writes:

>> +When TABLE contains a matching completion, return a list
>> +\(PREVIEW BEG END ALL EXIT-FN) where PREVIEW is the text to show
>> +in the completion preview, ALL is the list of all matching
>> +completion candidates, and EXIT-FN is either a function to call
>> +after inserting PREVIEW or nil.  When TABLE does not contain
>> +matching completions, or when there are multiple matching
>> +completions and `completion-preview-exact-match-only' is non-nil,
>> +return nil instead."
>
> It is better to use "if" here where you use "when".  "When" can be
> interpreted as a time-related condition, which is not what you want
> here.

Right, done in the updated patch (v2) below.

>> +(defun completion-preview--capf-wrapper (capf)
>> +  "Translate output of CAPF to properties for completion preview overlay.
>> +
>> +If CAPF returns a list (BEG END TABLE . PROPS), call
>
> If CAPF _returns_ something, it is probably a function.  But then why
> does the first sentence say "output of CAPF"? functions in ELisp don't
> "output" stuff.

Thanks, I've replaced "output" with "return value".

>> +`completion-preview--try-table' to check TABLE for matching
>> +completion candidates.  If `completion-preview--try-table'
>> +returns a non-nil value, return that value.  Otherwise, return a
>> +list with nil car which means that completion failed, unless
>> +PROPS includes the property `:exclusive' with value `no', in
>> +which case this function returns nil which means to try other
>> +functions from `completion-at-point-functions'."
>
> This basically tells in words what the code does.  But since the code
> is quite simple, I wonder why we need this in the doc string.  The
> disadvantage of having this in the doc string is that we'd need to
> update it each time the code changes.
>
> Instead, think if something in what the code does needs to be
> explained _beyond_ what the code itself says, like if you need to
> explain the reasons why the code does what it does, or why you access
> this or that property, and explain that -- in comments, not in the doc
> string.  The doc string should ideally be a higher-level description
> of the function's purpose and the meaning of its return values.

Makes sense, thanks.  I removed the lengthy description and added a
comment explaining the only non-obvious part.


Here's the updated patch:


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment;
 filename=v2-0001-Improve-and-add-tests-for-Completion-Preview-mode.patch

From 675019870e885ffe93944bc92e680a70eab99133 Mon Sep 17 00:00:00 2001
From: Eshel Yaron <me@HIDDEN>
Date: Sun, 19 Nov 2023 10:55:15 +0100
Subject: [PATCH v2] ; Improve and add tests for Completion Preview mode

Fix handling of capfs that return a function or signal an error,
respect the ':exclusive' completion property, fix lingering "exact"
face after deletion that makes the matches non-exact, and add tests.

* lisp/completion-preview.el (completion-preview--make-overlay): Only
reuse the previous 'after-string' if it has the right face.
(completion-preview--try-table)
(completion-preview--capf-wrapper): New functions.
(completion-preview--update): Use them.
* test/lisp/completion-preview-tests.el: New file.
---
 lisp/completion-preview.el            | 107 +++++++++------
 test/lisp/completion-preview-tests.el | 184 ++++++++++++++++++++++++++
 2 files changed, 250 insertions(+), 41 deletions(-)
 create mode 100644 test/lisp/completion-preview-tests.el

diff --git a/lisp/completion-preview.el b/lisp/completion-preview.el
index 6048d5be272..95410e2e5cd 100644
--- a/lisp/completion-preview.el
+++ b/lisp/completion-preview.el
@@ -155,7 +155,9 @@ completion-preview--make-overlay
     (setq completion-preview--overlay (make-overlay pos pos))
     (overlay-put completion-preview--overlay 'window (selected-window)))
   (let ((previous (overlay-get completion-preview--overlay 'after-string)))
-    (unless (and previous (string= previous string))
+    (unless (and previous (string= previous string)
+                 (eq (get-text-property 0 'face previous)
+                     (get-text-property 0 'face string)))
       (add-text-properties 0 1 '(cursor 1) string)
       (overlay-put completion-preview--overlay 'after-string string))
     completion-preview--overlay))
@@ -178,48 +180,71 @@ completion-preview--exit-function
     (completion-preview-active-mode -1)
     (when (functionp func) (apply func args))))
 
+(defun completion-preview--try-table (table beg end props)
+  "Check TABLE for a completion matching the text between BEG and END.
+
+PROPS is a property list with additional information about TABLE.
+See `completion-at-point-functions' for more details.
+
+If TABLE contains a matching completion, return a list
+\(PREVIEW BEG END ALL EXIT-FN) where PREVIEW is the text to show
+in the completion preview, ALL is the list of all matching
+completion candidates, and EXIT-FN is either a function to call
+after inserting PREVIEW or nil.  If TABLE does not contain
+matching completions, or if there are multiple matching
+completions and `completion-preview-exact-match-only' is non-nil,
+return nil instead."
+  (let* ((pred (plist-get props :predicate))
+         (exit-fn (completion-preview--exit-function
+                   (plist-get props :exit-function)))
+         (string (buffer-substring beg end))
+         (md (completion-metadata string table pred))
+         (sort-fn (or (completion-metadata-get md 'cycle-sort-function)
+                      (completion-metadata-get md 'display-sort-function)
+                      completion-preview-sort-function))
+         (all (let ((completion-lazy-hilit t))
+                (completion-all-completions string table pred
+                                            (- (point) beg) md)))
+         (last (last all))
+         (base (or (cdr last) 0))
+         (prefix (substring string base)))
+    (when last
+      (setcdr last nil)
+      (when-let ((sorted (funcall sort-fn
+                                  (delete prefix (all-completions prefix all)))))
+        (unless (and (cdr sorted) completion-preview-exact-match-only)
+          (list (propertize (substring (car sorted) (length prefix))
+                            'face (if (cdr sorted)
+                                      'completion-preview
+                                    'completion-preview-exact))
+                (+ beg base) end sorted exit-fn))))))
+
+(defun completion-preview--capf-wrapper (capf)
+  "Translate return value of CAPF to properties for completion preview overlay."
+  (unless (eq capf #'completion-preview--insert)
+    (let ((res (ignore-errors (funcall capf))))
+      (and (consp res)
+           (not (functionp res))
+           (seq-let (beg end table &rest plist) res
+             (or (completion-preview--try-table table beg end plist)
+                 (unless (eq 'no (plist-get plist :exclusive))
+                   ;; Return non-nil to exclude other capfs.
+                   '(nil))))))))
+
 (defun completion-preview--update ()
   "Update completion preview."
-  (seq-let (beg end table &rest plist)
-      (let ((completion-preview-insert-on-completion nil))
-        (run-hook-with-args-until-success 'completion-at-point-functions))
-    (when (and beg end table)
-      (let* ((pred (plist-get plist :predicate))
-             (exit-fn (completion-preview--exit-function
-                       (plist-get plist :exit-function)))
-             (string (buffer-substring beg end))
-             (md (completion-metadata string table pred))
-             (sort-fn (or (completion-metadata-get md 'cycle-sort-function)
-                          (completion-metadata-get md 'display-sort-function)
-                          completion-preview-sort-function))
-             (all (let ((completion-lazy-hilit t))
-                    (completion-all-completions string table pred
-                                                (- (point) beg) md)))
-             (last (last all))
-             (base (or (cdr last) 0))
-             (bbeg (+ beg base))
-             (prefix (substring string base)))
-        (when last
-          (setcdr last nil)
-          (let* ((filtered (remove prefix (all-completions prefix all)))
-                 (sorted (funcall sort-fn filtered))
-                 (multi (cadr sorted))  ; multiple candidates
-                 (cand (car sorted)))
-            (when (and cand
-                       (not (and multi
-                                 completion-preview-exact-match-only)))
-              (let* ((face (if multi
-                               'completion-preview
-                             'completion-preview-exact))
-                     (after (propertize (substring cand (length prefix))
-                                        'face face))
-                     (ov (completion-preview--make-overlay end after)))
-                (overlay-put ov 'completion-preview-beg bbeg)
-                (overlay-put ov 'completion-preview-end end)
-                (overlay-put ov 'completion-preview-index 0)
-                (overlay-put ov 'completion-preview-cands sorted)
-                (overlay-put ov 'completion-preview-exit-fn exit-fn)
-                (completion-preview-active-mode)))))))))
+  (seq-let (preview beg end all exit-fn)
+      (run-hook-wrapped
+       'completion-at-point-functions
+       #'completion-preview--capf-wrapper)
+    (when preview
+      (let ((ov (completion-preview--make-overlay end preview)))
+        (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-cands all)
+        (overlay-put ov 'completion-preview-exit-fn exit-fn)
+        (completion-preview-active-mode)))))
 
 (defun completion-preview--show ()
   "Show a new completion preview.
diff --git a/test/lisp/completion-preview-tests.el b/test/lisp/completion-preview-tests.el
new file mode 100644
index 00000000000..b5518e96254
--- /dev/null
+++ b/test/lisp/completion-preview-tests.el
@@ -0,0 +1,184 @@
+;;; completion-preview-tests.el --- tests for completion-preview.el -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+(require 'completion-preview)
+
+(defun completion-preview-tests--capf (completions &rest props)
+  (lambda ()
+    (when-let ((bounds (bounds-of-thing-at-point 'symbol)))
+      (append (list (car bounds) (cdr bounds) completions) props))))
+
+(defun completion-preview-tests--check-preview (string &optional exact)
+  "Check that the completion preview is showing STRING.
+
+If EXACT is non-nil, check that STRING has the
+`completion-preview-exact' face.  Otherwise check that STRING has
+the `completion-preview' face.
+
+If STRING is nil, check that there is no completion preview
+instead."
+  (if (not string)
+      (should (not completion-preview--overlay))
+    (should completion-preview--overlay)
+    (let ((after-string (completion-preview--get 'after-string)))
+      (should (string= after-string string))
+      (should (eq (get-text-property 0 'face after-string)
+                  (if exact
+                      'completion-preview-exact
+                    'completion-preview))))))
+
+(ert-deftest completion-preview ()
+  "Test Completion Preview mode."
+  (with-temp-buffer
+    (setq-local completion-at-point-functions
+                (list (completion-preview-tests--capf '("foobarbaz"))))
+
+    (insert "foo")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+
+    ;; Exact match
+    (completion-preview-tests--check-preview "barbaz" 'exact)
+
+    (insert "v")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+
+    ;; No match, no preview
+    (completion-preview-tests--check-preview nil)
+
+    (delete-char -1)
+    (let ((this-command 'delete-backward-char))
+      (completion-preview--post-command))
+
+    ;; Exact match again
+    (completion-preview-tests--check-preview "barbaz" 'exact)))
+
+(ert-deftest completion-preview-multiple-matches ()
+  "Test Completion Preview mode with multiple matching candidates."
+  (with-temp-buffer
+    (setq-local completion-at-point-functions
+                (list (completion-preview-tests--capf
+                       '("foobar" "foobaz"))))
+    (insert "foo")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+
+    ;; Multiple matches, the preview shows the first one
+    (completion-preview-tests--check-preview "bar")
+
+    (completion-preview-next-candidate 1)
+
+    ;; Next match
+    (completion-preview-tests--check-preview "baz")))
+
+(ert-deftest completion-preview-exact-match-only ()
+  "Test `completion-preview-exact-match-only'."
+  (with-temp-buffer
+    (setq-local completion-at-point-functions
+                (list (completion-preview-tests--capf
+                       '("spam" "foobar" "foobaz")))
+                completion-preview-exact-match-only t)
+    (insert "foo")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+
+    ;; Multiple matches, so no preview
+    (completion-preview-tests--check-preview nil)
+
+    (delete-region (point-min) (point-max))
+    (insert "spa")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+
+    ;; Exact match
+    (completion-preview-tests--check-preview "m" 'exact)))
+
+(ert-deftest completion-preview-function-capfs ()
+  "Test Completion Preview mode with capfs that return a function."
+  (with-temp-buffer
+    (setq-local completion-at-point-functions
+                (list
+                 (lambda () #'ignore)
+                 (completion-preview-tests--capf
+                  '("foobar" "foobaz"))))
+    (insert "foo")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+    (completion-preview-tests--check-preview "bar")))
+
+(ert-deftest completion-preview-non-exclusive-capfs ()
+  "Test Completion Preview mode with non-exclusive capfs."
+  (with-temp-buffer
+    (setq-local completion-at-point-functions
+                (list
+                 (completion-preview-tests--capf
+                  '("spam") :exclusive 'no)
+                 (completion-preview-tests--capf
+                  '("foobar" "foobaz") :exclusive 'no)
+                 (completion-preview-tests--capf
+                  '("foobarbaz"))))
+    (insert "foo")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+    (completion-preview-tests--check-preview "bar")
+    (setq-local completion-preview-exact-match-only t)
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+    (completion-preview-tests--check-preview "barbaz" 'exact)))
+
+(ert-deftest completion-preview-face-updates ()
+  "Test updating the face in completion preview when match is no longer exact."
+  (with-temp-buffer
+    (setq-local completion-at-point-functions
+                (list
+                 (completion-preview-tests--capf
+                  '("foobarbaz" "food"))))
+    (insert "foo")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+    (completion-preview-tests--check-preview "d")
+    (insert "b")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+    (completion-preview-tests--check-preview "arbaz" 'exact)
+    (delete-char -1)
+    (let ((this-command 'delete-backward-char))
+      (completion-preview--post-command))
+    (completion-preview-tests--check-preview "d")))
+
+(ert-deftest completion-preview-capf-errors ()
+  "Test Completion Preview mode with capfs that signal errors.
+
+`dabbrev-capf' is one example of such a capf."
+  (with-temp-buffer
+    (setq-local completion-at-point-functions
+                (list
+                 (lambda () (user-error "bad"))
+                 (completion-preview-tests--capf
+                  '("foobarbaz"))))
+    (insert "foo")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+    (completion-preview-tests--check-preview "barbaz" 'exact)))
+
+;;; completion-preview-tests.el ends here
-- 
2.42.0


--=-=-=--




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#67275; Package emacs. Full text available.

Message received at 67275 <at> debbugs.gnu.org:


Received: (at 67275) by debbugs.gnu.org; 19 Nov 2023 10:58:31 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sun Nov 19 05:58:31 2023
Received: from localhost ([127.0.0.1]:50100 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1r4fVX-0007hm-4U
	for submit <at> debbugs.gnu.org; Sun, 19 Nov 2023 05:58:31 -0500
Received: from eggs.gnu.org ([2001:470:142:3::10]:34568)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <eliz@HIDDEN>) id 1r4fVT-0007hW-Sn
 for 67275 <at> debbugs.gnu.org; Sun, 19 Nov 2023 05:58:30 -0500
Received: from fencepost.gnu.org ([2001:470:142:3::e])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <eliz@HIDDEN>)
 id 1r4fVM-000538-Sf; Sun, 19 Nov 2023 05:58:20 -0500
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org;
 s=fencepost-gnu-org; h=References:Subject:In-Reply-To:To:From:Date:
 mime-version; bh=G3MLiFKZ+LtKZSTpq1GJsc2NFt5lUro+wwv/k/pn/n0=; b=NxLy+RXKRH/U
 nzxp8B5RvQeEFvcCokloGN1XyMBlKoczfxl5EQX/oVQOeIlro/IvxAKiOnJShm7eJ5fJd0Gu2783P
 6vz31n4neIo9ba1mrRsbk/7mcOM0DJwS0mO/b9CEaSoeDLD0/dwNHHFQCPqbXwKKZ6Ql4nBJY7BmD
 l23P4WYPkbWsOObA5oOhMIZELSWCEpCCkSSiA52F2YtpVJdXGYt4irgjtsHPoxRzg9nxwQ/yvYIFR
 RVi0WmAk0OnnCMpv8h3XRlDDSNDtz2ocNVvkqsjHZmWUy6d7qLx3hciY3nQz6ypfPaaZH5nKsudQv
 2L4MOheRkAbR38+u0htcaA==;
Date: Sun, 19 Nov 2023 12:58:01 +0200
Message-Id: <8334x2ko1y.fsf@HIDDEN>
From: Eli Zaretskii <eliz@HIDDEN>
To: Eshel Yaron <me@HIDDEN>
In-Reply-To: <m1sf529gzw.fsf@HIDDEN> (bug-gnu-emacs@HIDDEN)
Subject: Re: bug#67275: [PATCH] ;
 Improve and add tests for Completion Preview mode
References: <m1sf529gzw.fsf@HIDDEN>
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 67275
Cc: 67275 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

> Date: Sun, 19 Nov 2023 11:25:55 +0100
> From:  Eshel Yaron via "Bug reports for GNU Emacs,
>  the Swiss army knife of text editors" <bug-gnu-emacs@HIDDEN>
> 
> +(defun completion-preview--try-table (table beg end props)
> +  "Check TABLE for a completion matching the text between BEG and END.
> +
> +PROPS is a property list with additional information about TABLE.
> +See `completion-at-point-functions' for more details.
> +
> +When TABLE contains a matching completion, return a list
> +\(PREVIEW BEG END ALL EXIT-FN) where PREVIEW is the text to show
> +in the completion preview, ALL is the list of all matching
> +completion candidates, and EXIT-FN is either a function to call
> +after inserting PREVIEW or nil.  When TABLE does not contain
> +matching completions, or when there are multiple matching
> +completions and `completion-preview-exact-match-only' is non-nil,
> +return nil instead."

It is better to use "if" here where you use "when".  "When" can be
interpreted as a time-related condition, which is not what you want
here.

> +(defun completion-preview--capf-wrapper (capf)
> +  "Translate output of CAPF to properties for completion preview overlay.
> +
> +If CAPF returns a list (BEG END TABLE . PROPS), call

If CAPF _returns_ something, it is probably a function.  But then why
does the first sentence say "output of CAPF"? functions in ELisp don't
"output" stuff.

> +`completion-preview--try-table' to check TABLE for matching
> +completion candidates.  If `completion-preview--try-table'
> +returns a non-nil value, return that value.  Otherwise, return a
> +list with nil car which means that completion failed, unless
> +PROPS includes the property `:exclusive' with value `no', in
> +which case this function returns nil which means to try other
> +functions from `completion-at-point-functions'."

This basically tells in words what the code does.  But since the code
is quite simple, I wonder why we need this in the doc string.  The
disadvantage of having this in the doc string is that we'd need to
update it each time the code changes.

Instead, think if something in what the code does needs to be
explained _beyond_ what the code itself says, like if you need to
explain the reasons why the code does what it does, or why you access
this or that property, and explain that -- in comments, not in the doc
string.  The doc string should ideally be a higher-level description
of the function's purpose and the meaning of its return values.

Thanks.




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#67275; Package emacs. Full text available.

Message received at submit <at> debbugs.gnu.org:


Received: (at submit) by debbugs.gnu.org; 19 Nov 2023 10:26:13 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sun Nov 19 05:26:13 2023
Received: from localhost ([127.0.0.1]:50084 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1r4f0G-0006mX-L9
	for submit <at> debbugs.gnu.org; Sun, 19 Nov 2023 05:26:13 -0500
Received: from lists.gnu.org ([2001:470:142::17]:50720)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <me@HIDDEN>) id 1r4f0C-0006mI-Tz
 for submit <at> debbugs.gnu.org; Sun, 19 Nov 2023 05:26:11 -0500
Received: from eggs.gnu.org ([2001:470:142:3::10])
 by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <me@HIDDEN>) id 1r4f06-0004Lv-27
 for bug-gnu-emacs@HIDDEN; Sun, 19 Nov 2023 05:26:02 -0500
Received: from mail.eshelyaron.com ([107.175.124.16] helo=eshelyaron.com)
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <me@HIDDEN>) id 1r4f03-0000gB-Mr
 for bug-gnu-emacs@HIDDEN; Sun, 19 Nov 2023 05:26:01 -0500
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=eshelyaron.com;
 s=mail; t=1700389558;
 bh=wgAJx+7QeDJKaJBIaKLPK4jtZ8LXqaZ4F+O0mSz+tJM=;
 h=From:To:Subject:Date:From;
 b=mn46FAHUiEYNOjEMdQq2pBoM6u2KpkjbUtni938OUkhVuvZ7U1lxZaj/tGxo34FIz
 6F3mpdorDG0NWxR5PmxMsEap44zvTM0NQmVdx7YzLF1z3gQU9Z2lcSz7IeBGemsycM
 QTVkdR7qsZwaqSpMURUknjAZwsv2oL02xq80Z1VkYxiCZcSKZpy5WADHl0Aq+QJJy3
 4vm1ht4f1Rq7DbvlzqDWPvBZwdtHqLSqAYQV9tOgGECPfWN5U1ODA9tWu8eoS5anYb
 Nz7/mlEUYNpayCXCa9JNKWQfNDh6k7wv1k5ioED2mVxz+qWiXViTBzDeMe9d2mzW+T
 Z1xw/ilM0EHeA==
From: Eshel Yaron <me@HIDDEN>
To: bug-gnu-emacs@HIDDEN
Subject: [PATCH] ; Improve and add tests for Completion Preview mode
X-Hashcash: 1:20:231119:bug-gnu-emacs@HIDDEN::RpwUaytY3G9Pyr1n:06SO
Date: Sun, 19 Nov 2023 11:25:55 +0100
Message-ID: <m1sf529gzw.fsf@HIDDEN>
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="=-=-="
Received-SPF: pass client-ip=107.175.124.16; envelope-from=me@HIDDEN;
 helo=eshelyaron.com
X-Spam_score_int: -20
X-Spam_score: -2.1
X-Spam_bar: --
X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1,
 DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, SPF_HELO_PASS=-0.001,
 SPF_PASS=-0.001, T_SCC_BODY_TEXT_LINE=-0.01 autolearn=ham autolearn_force=no
X-Spam_action: no action
X-Spam-Score: 0.9 (/)
X-Debbugs-Envelope-To: submit
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.1 (/)

--=-=-=
Content-Type: text/plain

Tags: patch

This patch makes Completion Preview mode more robust in face of
misbehaving `completion-at-point-functions`, and adds some tests.


Thanks,

Eshel


--=-=-=
Content-Type: text/patch
Content-Disposition: attachment;
 filename=0001-Improve-and-add-tests-for-Completion-Preview-mode.patch

From f6a4eac72e8439ad61a2d71e8e376db0d9d55064 Mon Sep 17 00:00:00 2001
From: Eshel Yaron <me@HIDDEN>
Date: Sun, 19 Nov 2023 10:55:15 +0100
Subject: [PATCH] ; Improve and add tests for Completion Preview mode

Fix handling of capfs that return a function or signal an error,
respect the ':exclusive' completion property, fix lingering "exact"
face after deletion that makes the matches non-exact, and add tests.

* lisp/completion-preview.el (completion-preview--make-overlay): Only
reuse the previous 'after-string' if it has the right face.
(completion-preview--try-table)
(completion-preview--capf-wrapper): New functions.
(completion-preview--update): Use them.
* test/lisp/completion-preview-tests.el: New file.
---
 lisp/completion-preview.el            | 114 ++++++++++------
 test/lisp/completion-preview-tests.el | 184 ++++++++++++++++++++++++++
 2 files changed, 257 insertions(+), 41 deletions(-)
 create mode 100644 test/lisp/completion-preview-tests.el

diff --git a/lisp/completion-preview.el b/lisp/completion-preview.el
index 6048d5be272..2b81dc5cd61 100644
--- a/lisp/completion-preview.el
+++ b/lisp/completion-preview.el
@@ -155,7 +155,9 @@ completion-preview--make-overlay
     (setq completion-preview--overlay (make-overlay pos pos))
     (overlay-put completion-preview--overlay 'window (selected-window)))
   (let ((previous (overlay-get completion-preview--overlay 'after-string)))
-    (unless (and previous (string= previous string))
+    (unless (and previous (string= previous string)
+                 (eq (get-text-property 0 'face previous)
+                     (get-text-property 0 'face string)))
       (add-text-properties 0 1 '(cursor 1) string)
       (overlay-put completion-preview--overlay 'after-string string))
     completion-preview--overlay))
@@ -178,48 +180,78 @@ completion-preview--exit-function
     (completion-preview-active-mode -1)
     (when (functionp func) (apply func args))))
 
+(defun completion-preview--try-table (table beg end props)
+  "Check TABLE for a completion matching the text between BEG and END.
+
+PROPS is a property list with additional information about TABLE.
+See `completion-at-point-functions' for more details.
+
+When TABLE contains a matching completion, return a list
+\(PREVIEW BEG END ALL EXIT-FN) where PREVIEW is the text to show
+in the completion preview, ALL is the list of all matching
+completion candidates, and EXIT-FN is either a function to call
+after inserting PREVIEW or nil.  When TABLE does not contain
+matching completions, or when there are multiple matching
+completions and `completion-preview-exact-match-only' is non-nil,
+return nil instead."
+  (let* ((pred (plist-get props :predicate))
+         (exit-fn (completion-preview--exit-function
+                   (plist-get props :exit-function)))
+         (string (buffer-substring beg end))
+         (md (completion-metadata string table pred))
+         (sort-fn (or (completion-metadata-get md 'cycle-sort-function)
+                      (completion-metadata-get md 'display-sort-function)
+                      completion-preview-sort-function))
+         (all (let ((completion-lazy-hilit t))
+                (completion-all-completions string table pred
+                                            (- (point) beg) md)))
+         (last (last all))
+         (base (or (cdr last) 0))
+         (prefix (substring string base)))
+    (when last
+      (setcdr last nil)
+      (when-let ((sorted (funcall sort-fn
+                                  (delete prefix (all-completions prefix all)))))
+        (unless (and (cdr sorted) completion-preview-exact-match-only)
+          (list (propertize (substring (car sorted) (length prefix))
+                            'face (if (cdr sorted)
+                                      'completion-preview
+                                    'completion-preview-exact))
+                (+ beg base) end sorted exit-fn))))))
+
+(defun completion-preview--capf-wrapper (capf)
+  "Translate output of CAPF to properties for completion preview overlay.
+
+If CAPF returns a list (BEG END TABLE . PROPS), call
+`completion-preview--try-table' to check TABLE for matching
+completion candidates.  If `completion-preview--try-table'
+returns a non-nil value, return that value.  Otherwise, return a
+list with nil car which means that completion failed, unless
+PROPS includes the property `:exclusive' with value `no', in
+which case this function returns nil which means to try other
+functions from `completion-at-point-functions'."
+  (unless (eq capf #'completion-preview--insert)
+    (let ((res (ignore-errors (funcall capf))))
+      (and (consp res)
+           (not (functionp res))
+           (seq-let (beg end table &rest plist) res
+             (or (completion-preview--try-table table beg end plist)
+                 (and (not (eq 'no (plist-get plist :exclusive))) '(nil))))))))
+
 (defun completion-preview--update ()
   "Update completion preview."
-  (seq-let (beg end table &rest plist)
-      (let ((completion-preview-insert-on-completion nil))
-        (run-hook-with-args-until-success 'completion-at-point-functions))
-    (when (and beg end table)
-      (let* ((pred (plist-get plist :predicate))
-             (exit-fn (completion-preview--exit-function
-                       (plist-get plist :exit-function)))
-             (string (buffer-substring beg end))
-             (md (completion-metadata string table pred))
-             (sort-fn (or (completion-metadata-get md 'cycle-sort-function)
-                          (completion-metadata-get md 'display-sort-function)
-                          completion-preview-sort-function))
-             (all (let ((completion-lazy-hilit t))
-                    (completion-all-completions string table pred
-                                                (- (point) beg) md)))
-             (last (last all))
-             (base (or (cdr last) 0))
-             (bbeg (+ beg base))
-             (prefix (substring string base)))
-        (when last
-          (setcdr last nil)
-          (let* ((filtered (remove prefix (all-completions prefix all)))
-                 (sorted (funcall sort-fn filtered))
-                 (multi (cadr sorted))  ; multiple candidates
-                 (cand (car sorted)))
-            (when (and cand
-                       (not (and multi
-                                 completion-preview-exact-match-only)))
-              (let* ((face (if multi
-                               'completion-preview
-                             'completion-preview-exact))
-                     (after (propertize (substring cand (length prefix))
-                                        'face face))
-                     (ov (completion-preview--make-overlay end after)))
-                (overlay-put ov 'completion-preview-beg bbeg)
-                (overlay-put ov 'completion-preview-end end)
-                (overlay-put ov 'completion-preview-index 0)
-                (overlay-put ov 'completion-preview-cands sorted)
-                (overlay-put ov 'completion-preview-exit-fn exit-fn)
-                (completion-preview-active-mode)))))))))
+  (seq-let (preview beg end all exit-fn)
+      (run-hook-wrapped
+       'completion-at-point-functions
+       #'completion-preview--capf-wrapper)
+    (when preview
+      (let ((ov (completion-preview--make-overlay end preview)))
+        (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-cands all)
+        (overlay-put ov 'completion-preview-exit-fn exit-fn)
+        (completion-preview-active-mode)))))
 
 (defun completion-preview--show ()
   "Show a new completion preview.
diff --git a/test/lisp/completion-preview-tests.el b/test/lisp/completion-preview-tests.el
new file mode 100644
index 00000000000..b5518e96254
--- /dev/null
+++ b/test/lisp/completion-preview-tests.el
@@ -0,0 +1,184 @@
+;;; completion-preview-tests.el --- tests for completion-preview.el -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+(require 'completion-preview)
+
+(defun completion-preview-tests--capf (completions &rest props)
+  (lambda ()
+    (when-let ((bounds (bounds-of-thing-at-point 'symbol)))
+      (append (list (car bounds) (cdr bounds) completions) props))))
+
+(defun completion-preview-tests--check-preview (string &optional exact)
+  "Check that the completion preview is showing STRING.
+
+If EXACT is non-nil, check that STRING has the
+`completion-preview-exact' face.  Otherwise check that STRING has
+the `completion-preview' face.
+
+If STRING is nil, check that there is no completion preview
+instead."
+  (if (not string)
+      (should (not completion-preview--overlay))
+    (should completion-preview--overlay)
+    (let ((after-string (completion-preview--get 'after-string)))
+      (should (string= after-string string))
+      (should (eq (get-text-property 0 'face after-string)
+                  (if exact
+                      'completion-preview-exact
+                    'completion-preview))))))
+
+(ert-deftest completion-preview ()
+  "Test Completion Preview mode."
+  (with-temp-buffer
+    (setq-local completion-at-point-functions
+                (list (completion-preview-tests--capf '("foobarbaz"))))
+
+    (insert "foo")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+
+    ;; Exact match
+    (completion-preview-tests--check-preview "barbaz" 'exact)
+
+    (insert "v")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+
+    ;; No match, no preview
+    (completion-preview-tests--check-preview nil)
+
+    (delete-char -1)
+    (let ((this-command 'delete-backward-char))
+      (completion-preview--post-command))
+
+    ;; Exact match again
+    (completion-preview-tests--check-preview "barbaz" 'exact)))
+
+(ert-deftest completion-preview-multiple-matches ()
+  "Test Completion Preview mode with multiple matching candidates."
+  (with-temp-buffer
+    (setq-local completion-at-point-functions
+                (list (completion-preview-tests--capf
+                       '("foobar" "foobaz"))))
+    (insert "foo")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+
+    ;; Multiple matches, the preview shows the first one
+    (completion-preview-tests--check-preview "bar")
+
+    (completion-preview-next-candidate 1)
+
+    ;; Next match
+    (completion-preview-tests--check-preview "baz")))
+
+(ert-deftest completion-preview-exact-match-only ()
+  "Test `completion-preview-exact-match-only'."
+  (with-temp-buffer
+    (setq-local completion-at-point-functions
+                (list (completion-preview-tests--capf
+                       '("spam" "foobar" "foobaz")))
+                completion-preview-exact-match-only t)
+    (insert "foo")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+
+    ;; Multiple matches, so no preview
+    (completion-preview-tests--check-preview nil)
+
+    (delete-region (point-min) (point-max))
+    (insert "spa")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+
+    ;; Exact match
+    (completion-preview-tests--check-preview "m" 'exact)))
+
+(ert-deftest completion-preview-function-capfs ()
+  "Test Completion Preview mode with capfs that return a function."
+  (with-temp-buffer
+    (setq-local completion-at-point-functions
+                (list
+                 (lambda () #'ignore)
+                 (completion-preview-tests--capf
+                  '("foobar" "foobaz"))))
+    (insert "foo")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+    (completion-preview-tests--check-preview "bar")))
+
+(ert-deftest completion-preview-non-exclusive-capfs ()
+  "Test Completion Preview mode with non-exclusive capfs."
+  (with-temp-buffer
+    (setq-local completion-at-point-functions
+                (list
+                 (completion-preview-tests--capf
+                  '("spam") :exclusive 'no)
+                 (completion-preview-tests--capf
+                  '("foobar" "foobaz") :exclusive 'no)
+                 (completion-preview-tests--capf
+                  '("foobarbaz"))))
+    (insert "foo")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+    (completion-preview-tests--check-preview "bar")
+    (setq-local completion-preview-exact-match-only t)
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+    (completion-preview-tests--check-preview "barbaz" 'exact)))
+
+(ert-deftest completion-preview-face-updates ()
+  "Test updating the face in completion preview when match is no longer exact."
+  (with-temp-buffer
+    (setq-local completion-at-point-functions
+                (list
+                 (completion-preview-tests--capf
+                  '("foobarbaz" "food"))))
+    (insert "foo")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+    (completion-preview-tests--check-preview "d")
+    (insert "b")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+    (completion-preview-tests--check-preview "arbaz" 'exact)
+    (delete-char -1)
+    (let ((this-command 'delete-backward-char))
+      (completion-preview--post-command))
+    (completion-preview-tests--check-preview "d")))
+
+(ert-deftest completion-preview-capf-errors ()
+  "Test Completion Preview mode with capfs that signal errors.
+
+`dabbrev-capf' is one example of such a capf."
+  (with-temp-buffer
+    (setq-local completion-at-point-functions
+                (list
+                 (lambda () (user-error "bad"))
+                 (completion-preview-tests--capf
+                  '("foobarbaz"))))
+    (insert "foo")
+    (let ((this-command 'self-insert-command))
+      (completion-preview--post-command))
+    (completion-preview-tests--check-preview "barbaz" 'exact)))
+
+;;; completion-preview-tests.el ends here
-- 
2.42.0


--=-=-=--




Acknowledgement sent to Eshel Yaron <me@HIDDEN>:
New bug report received and forwarded. Copy sent to bug-gnu-emacs@HIDDEN. Full text available.
Report forwarded to bug-gnu-emacs@HIDDEN:
bug#67275; Package emacs. Full text available.
Please note: This is a static page, with minimal formatting, updated once a day.
Click here to see this page with the latest information and nicer formatting.
Last modified: Mon, 20 Nov 2023 12:30:02 UTC

GNU bug tracking system
Copyright (C) 1999 Darren O. Benham, 1997 nCipher Corporation Ltd, 1994-97 Ian Jackson.