Subject: bug#79377: [PATCH] Return case common to all completions in try-completion

From: Spencer Baugh <[email-masked]>
Date: Wed, 03 Sep 2025 09:07:30 -0400

Tags: patch

There are a few bugs with completion-pcm-try-completion not returning
text whose case matches the completions, even when the case is identical
between all the completions.  I added some tests covering them.

Ultimately, these bugs are nicely fixed by an improvement to
Ftry_completion; the removed conditional (and its associated comment) in
Ftry_completion didn't actually improve behavior, but instead made case
behavior worse.  I believe the bad behavior which this conditional was
originally attempting to stop was removed at some point by other
enhancements to Ftry_completion's completion-ignore-case behavior, so
the conditional isn't necessary anymore.

In GNU Emacs 30.1.90 (build 43, x86_64-pc-linux-gnu, X toolkit, cairo
 version 1.15.12, Xaw scroll bars) of 2025-09-03 built on
 igm-qws-u22796a
Repository revision: 8a831d9c110ea4dd349444de8f99d7cee10c5273
Repository branch: emacs-30
Windowing system distributor 'The X.Org Foundation', version 11.0.12011000
System Description: Rocky Linux 8.10 (Green Obsidian)

Configured using:
 'configure --with-x-toolkit=lucid --without-gpm --without-gconf
 --without-selinux --without-imagemagick --with-modules --with-gif=no
 --with-cairo --with-rsvg --without-compress-install --with-tree-sitter
 --with-native-compilation=aot
 PKG_CONFIG_PATH=/usr/local/home/garnish/libtree-sitter/0.22.6-1/lib/pkgconfig/'

>From 1064a06c656bd10db29c2749ce7b87b1100a78b1 Mon Sep 17 00:00:00 2001
From: Spencer Baugh <[email-masked]>
Date: Wed, 3 Sep 2025 08:48:05 -0400
Subject: [PATCH] Return case common to all completions in try-completion

When completion-ignore-case is non-nil, if all completions share
a common prefix ignoring case, try-completion has always
returned that.  Now, if all completions also share a common
prefix including case, try-completion includes that common
prefix in its return value.

* lisp/minibuffer.el (completion-pcm--merge-completions): Always
use return value from try-completion, which may change case.
* src/minibuf.c (Ftry_completion): Return the common prefix
which changes case.
* test/lisp/minibuffer-tests.el (completion-pcm-bug4219)
(completion-substring-test-5): New tests.
---
 lisp/minibuffer.el            |  2 +-
 src/minibuf.c                 |  6 ------
 test/lisp/minibuffer-tests.el | 28 +++++++++++++++++++++++++++-
 3 files changed, 28 insertions(+), 8 deletions(-)

diff --git a/lisp/minibuffer.el b/lisp/minibuffer.el
index f3bca47f271..036e347ae22 100644
--- a/lisp/minibuffer.el
+++ b/lisp/minibuffer.el
@@ -4705,7 +4705,7 @@ completion-pcm--merge-completions
                   ;; `prefix' only wants to include the fixed part before the
                   ;; wildcard, not the result of growing that fixed part.
                   (when (seq-some (lambda (elem) (eq elem 'prefix)) wildcards)
-                    (setq prefix fixed))
+                    (setq prefix (substring prefix 0 (length fixed))))
                   (push prefix res)
                   ;; Push all the wildcards in this stretch, to preserve `point' and
                   ;; `star' wildcards before ELEM.
diff --git a/src/minibuf.c b/src/minibuf.c
index 6d96160a851..53bc905af6f 100644
--- a/src/minibuf.c
+++ b/src/minibuf.c
@@ -1820,12 +1820,6 @@ DEFUN ("try-completion", Ftry_completion, Stry_completion, 2, 3, 0,
 
   if (NILP (bestmatch))
     return Qnil;		/* No completions found.  */
-  /* If we are ignoring case, and there is no exact match,
-     and no additional text was supplied,
-     don't change the case of what the user typed.  */
-  if (completion_ignore_case && bestmatchsize == SCHARS (string)
-      && SCHARS (bestmatch) > bestmatchsize)
-    return string;
 
   /* Return t if the supplied string is an exact match (counting case);
      it does not require any change to be made.  */
diff --git a/test/lisp/minibuffer-tests.el b/test/lisp/minibuffer-tests.el
index de1a98c8189..a0c6a2fb8d5 100644
--- a/test/lisp/minibuffer-tests.el
+++ b/test/lisp/minibuffer-tests.el
@@ -345,7 +345,24 @@ completion-pcm-bug4219
   (should (equal
            (let ((completion-ignore-case t))
              (completion-pcm-try-completion "a" '("ABC" "ABD") nil 1))
-           '("AB" . 2))))
+           '("AB" . 2)))
+  ;; Even when the text isn't growing.
+  (should (equal
+           (let ((completion-ignore-case t))
+             (completion-pcm-try-completion "ab" '("ABC" "ABD") nil 2))
+           '("AB" . 2)))
+  ;; Or when point is in the middle of the region changing case.
+  (should (equal
+           (let ((completion-ignore-case t))
+             (completion-pcm-try-completion "ab" '("ABC" "ABD") nil 1))
+           '("AB" . 2)))
+  ;; We don't change case if it doesn't match all of the completions, though.
+  (should (equal
+           (let ((completion-ignore-case t)) (try-completion "a" '("ax" "Ay")))
+           "a"))
+  (should (equal
+           (let ((completion-ignore-case t)) (try-completion "a" '("Ay" "ax")))
+           "a")))
 
 (ert-deftest completion-substring-test-1 ()
   ;; One third of a match!
@@ -409,6 +426,15 @@ completion-substring-test-5
   (should (equal
            (completion-pcm--merge-try '("a" prefix "b") '("axb" "ayb") "" "")
            '("ab" . 2)))
+  ;; Letter-casing from the completions on the common prefix is still applied.
+  (should (equal
+           (let ((completion-ignore-case t))
+             (completion-pcm--merge-try '("a" prefix "b") '("Axb" "Ayb") "" ""))
+           '("Ab" . 2)))
+  (should (equal
+           (let ((completion-ignore-case t))
+             (completion-pcm--merge-try '("a" prefix "b") '("AAxb" "AAyb") "" ""))
+           '("Ab" . 2)))
   ;; substring completion should successfully complete the entire string
   (should (equal
            (completion-substring-try-completion "b" '("ab" "ab") nil 0)
-- 
2.43.7