GNU bug report logs - #79265
[PATCH] Treat point more consistently in PCM completion

Previous Next

Package: emacs;

Reported by: Spencer Baugh <sbaugh <at> janestreet.com>

Date: Mon, 18 Aug 2025 18:54:01 UTC

Severity: normal

Tags: patch

Done: Dmitry Gutov <dmitry <at> gutov.dev>

Full log


View this message in rfc822 format

From: Spencer Baugh <sbaugh <at> janestreet.com>
To: Dmitry Gutov <dmitry <at> gutov.dev>
Cc: Stefan Monnier <monnier <at> iro.umontreal.ca>, 79265 <at> debbugs.gnu.org
Subject: bug#79265: [PATCH] Treat point more consistently in PCM completion
Date: Tue, 19 Aug 2025 12:03:22 -0400
[Message part 1 (text/plain, inline)]
Dmitry Gutov <dmitry <at> gutov.dev> writes:

> On 18/08/2025 21:53, Spencer Baugh wrote:
>> Properly fix bug#38458, which is fundamentally an issue with
>> completion-ignore-case, by checking if the completions are
>> unique ignoring case.  When the completions are unique, the
>> normal code to delete a wildcard naturally causes point to be
>> moved to the end of the minibuffer, which is the correct
>> behavior.
>> Now that the bug is fixed properly, remove a hack which
>> previously was used to "fix" it, which made point behave
>> inconsistently if it was in the middle of the minibuffer versus
>> at the end of the minibuffer.
>
> Thanks, the description makes sense and the tests are great to have.
>
> I wonder if Stefan will want to comment though.
>
> Just one thing:
>
>> +                                      ;; They're all the same string, ignoring case.
>> +                                      (seq-every-p
>> +                                       (lambda (elem) (string-equal-ignore-case elem prefix))
>> +                                       comps)))))
>
> Is this performance-sensitive? Using a less abstracted function like
> cl-every of even a (cl-loop for ... always ...) could be faster.
>
> There is a (dolist) above it which iterates across segments of the
> pattern, so whatever overhead is added could be multiplied; OT2H I
> haven't tested it myself yet, maybe all bottlenecks are somewhere
> else.

Hm, good point.  After thinking about it more, I realized that the
second try-completion call above somewhat duplicates the check I added,
and that I could refactor in general to improve performance here.

So I reworked the change a bit to combine my check with the second
try-completion call.  This should have better performance even though we
aren't using the C implementation of try-completion anymore, since we're
now doing much less work than try-completion does.

(I also refactored to use a cond instead of (or (and ...) (and ...)),
which I think makes this logic a fair bit clearer)

[0001-Treat-point-more-consistently-in-PCM-completion.patch (text/x-patch, inline)]
From 9465045d19a8676847cc8932ca66a40aabe2af59 Mon Sep 17 00:00:00 2001
From: Spencer Baugh <sbaugh <at> janestreet.com>
Date: Mon, 18 Aug 2025 14:50:44 -0400
Subject: [PATCH] Treat point more consistently in PCM completion

Properly fix bug#38458, which is fundamentally an issue with
completion-ignore-case, by checking if the completions are
unique ignoring case.  When the completions are unique, the
normal code to delete a wildcard naturally causes point to be
moved to the end of the minibuffer, which is the correct
behavior.

Now that the bug is fixed properly, remove a hack which
previously was used to "fix" it, which made point behave
inconsistently if it was in the middle of the minibuffer versus
at the end of the minibuffer.

* lisp/minibuffer.el (completion-pcm--merge-completions):
Respect completion-ignore-case when checking for completion
uniqueness. (bug#79265)
(completion-pcm--string->pattern)
(completion-pcm--optimize-pattern): Allow point at the end of
the pattern.
* test/lisp/minibuffer-tests.el (completion-table-test-quoting)
(completion-test--pcm-bug38458, completion-pcm-test-8): Update
tests for more correct behavior.
---
 lisp/minibuffer.el            | 25 +++++++++++++++----------
 test/lisp/minibuffer-tests.el | 28 +++++++++++++++++++++-------
 2 files changed, 36 insertions(+), 17 deletions(-)

diff --git a/lisp/minibuffer.el b/lisp/minibuffer.el
index 689cda7edab..c7efba10fec 100644
--- a/lisp/minibuffer.el
+++ b/lisp/minibuffer.el
@@ -4237,7 +4237,7 @@ completion-pcm--string->pattern
   "Split STRING into a pattern.
 A pattern is a list where each element is either a string
 or a symbol, see `completion-pcm--merge-completions'."
-  (if (and point (< point (length string)))
+  (if (and point (<= point (length string)))
       (let ((prefix (substring string 0 point))
             (suffix (substring string point)))
         (append (completion-pcm--string->pattern prefix)
@@ -4298,12 +4298,6 @@ completion-pcm--optimize-pattern
       (pcase p
         (`(,(or 'any 'any-delim) ,(or 'any 'point) . ,_)
          (setq p (cdr p)))
-        ;; This is not just a performance improvement: it turns a
-        ;; terminating `point' into an implicit `any', which affects
-        ;; the final position of point (because `point' gets turned
-        ;; into a non-greedy ".*?" regexp whereas we need it to be
-        ;; greedy when it's at the end, see bug#38458).
-        (`(point) (setq p nil)) ;Implicit terminating `any'.
         (_ (push (pop p) n))))
     (nreverse n)))
 
@@ -4755,9 +4749,20 @@ completion-pcm--merge-completions
               ;; In practice, it doesn't seem to make any difference.
               (setq ccs (nreverse ccs))
               (let* ((prefix (try-completion fixed comps))
-                     (unique (or (and (eq prefix t) (setq prefix fixed))
-                                 (and (stringp prefix)
-                                      (eq t (try-completion prefix comps))))))
+                     (unique
+                      (cond
+                       ((eq prefix t)
+                        (setq prefix fixed)
+                        t)
+                       ((stringp prefix)
+                        ;; `try-completion' is almost this, but it doesn't
+                        ;; handle `completion-ignore-case' the way we want.
+                        (not (cl-some
+                              (lambda (comp)
+                                ;; PREFIX could complete to COMP, so it's not unique.
+                                (and (string-prefix-p prefix comp completion-ignore-case)
+                                     (not (= (length prefix) (length comp)))))
+                              comps))))))
                 ;; If there's only one completion, `elem' is not useful
                 ;; any more: it can only match the empty string.
                 ;; FIXME: in some cases, it may be necessary to turn an
diff --git a/test/lisp/minibuffer-tests.el b/test/lisp/minibuffer-tests.el
index f9a26d17e58..59b72899e22 100644
--- a/test/lisp/minibuffer-tests.el
+++ b/test/lisp/minibuffer-tests.el
@@ -100,10 +100,10 @@ completion-table-test-quoting
                      ;; Test that $$ in input is properly unquoted.
                      ("data/m-cttq$$t" "data/minibuffer-test-cttq$$tion")
                      ;; Test that env-vars are preserved.
-                     ("lisp/c${CTTQ1}et/se-u" "lisp/c${CTTQ1}et/semantic-utest")
-                     ("lisp/ced${CTTQ2}se-u" "lisp/ced${CTTQ2}semantic-utest")
+                     ("lisp/c${CTTQ1}et/se-u-c" "lisp/c${CTTQ1}et/semantic-utest-c.test")
+                     ("lisp/ced${CTTQ2}se-u-c" "lisp/ced${CTTQ2}semantic-utest-c.test")
                      ;; Test that env-vars don't prevent partial-completion.
-                     ("lis/c${CTTQ1}/se-u" "lisp/c${CTTQ1}et/semantic-utest")
+                     ("lis/c${CTTQ1}/se-u-c" "lisp/c${CTTQ1}et/semantic-utest-c.test")
                      ))
       (should (equal (completion-try-completion input
                                                 #'completion--file-name-table
@@ -118,11 +118,11 @@ completion-table-test-quoting
                        ;; When an env var is in the completion bounds, try-completion
                        ;; won't change letter case.
                        ("lisp/c${CTTQ1}E" "lisp/c${CTTQ1}Et/")
-                       ("lisp/ced${CTTQ2}SE-U" "lisp/ced${CTTQ2}SEmantic-utest")
+                       ("lisp/ced${CTTQ2}SE-U-c" "lisp/ced${CTTQ2}SEmantic-utest-c.test")
                        ;; If the env var is before the completion bounds, try-completion
                        ;; *will* change letter case.
-                       ("lisp/c${CTTQ1}et/SE-U" "lisp/c${CTTQ1}et/semantic-utest")
-                       ("lis/c${CTTQ1}/SE-U" "lisp/c${CTTQ1}et/semantic-utest")
+                       ("lisp/c${CTTQ1}et/SE-U-c" "lisp/c${CTTQ1}et/semantic-utest-c.test")
+                       ("lis/c${CTTQ1}/SE-U-c" "lisp/c${CTTQ1}et/semantic-utest-c.test")
                        ))
         (should (equal (car (completion-try-completion input
                                                        #'completion--file-name-table
@@ -224,7 +224,11 @@ completion-test--pcm-bug38458
                    (completion-pcm--merge-try '("tes" point "ing")
                                               '("Testing" "testing")
                                               "" ""))
-           '("testing" . 4))))
+                 '("testing" . 7)))
+  (should (equal
+           (let ((completion-ignore-case t))
+             (completion-pcm-try-completion "tes" '("Testing" "testing") nil 3))
+           '("testing" . 7))))
 
 (ert-deftest completion-pcm-test-1 ()
   ;; Point is at end, this does not match anything
@@ -318,6 +322,16 @@ completion-pcm-test-7
             '(prefix any "bar" any) '("xbarxfoo" "ybaryfoo") "" "")
            '("bar" . 3))))
 
+(ert-deftest completion-pcm-test-8 ()
+  ;; try-completion inserts the common prefix and suffix at point.
+  (should (equal (completion-pcm-try-completion
+                  "r" '("fooxbar" "fooybar") nil 0)
+                 '("foobar" . 3)))
+  ;; Even if point is at the end of the minibuffer.
+  (should (equal (completion-pcm-try-completion
+                  "" '("fooxbar" "fooybar") nil 0)
+                 '("foobar" . 3))))
+
 (ert-deftest completion-substring-test-1 ()
   ;; One third of a match!
   (should (equal
-- 
2.43.7


This bug report was last modified 9 days ago.

Previous Next


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