Subject: bug#78323: Regression in case-insensitive filename completion

From: Spencer Baugh <[email-masked]>
Date: Tue, 13 May 2025 12:23:36 -0400

Eli Zaretskii <[email-masked]> writes:

>> From: Stefan Monnier <[email-masked]>
>> Cc: Daniel Colascione <[email-masked]>,  [email-masked],
>>   [email-masked]
>> Date: Mon, 12 May 2025 10:42:47 -0400
>> 
>> >> >> With read-file-name-completion-ignore-case true, commit
>> >> >> 509cbe1c35b3dd005a53ac041f9c87ee53b8e115 breaks existing completion
>> >> >> behavior.  Suppose we have file named fooBarQux.  Previously, if I'd
>> >> >> typed "foobar" and TAB while reading a filename, then the completion
>> >> >> would be "fooBarQux".  Now, it completes as "foobarQux", which is wrong
>> >> >> and breaks, among other things, vc.
>> >> > Spencer, could you please look into fixing this regression?
>> >> What do you think of reverting this change while Spencer finds the cause
>> >> of the problem?
>> > I wouldn't mind, but then I don't use these features.  Let's hear
>> > Stefan's opinion first.
>> 
>> I'd first like to hear Spencer's opinion on how quickly he might be able to
>> fix it or if it indeed breaks his assumptions too hard (so we'd need
>> a different approach, in which case we may as well revert the change first).
>
> So, Spencer, the eyes of all the world are upon you.

Sorry for the delay, I was away this weekend.  Thanks for your patience.

The attached patch should fix the issue; it's what I have installed at
my site.

>From 2418e67a71494396e8dbfd7fd5eabfb52a87198b Mon Sep 17 00:00:00 2001
From: Spencer Baugh <[email-masked]>
Date: Thu, 1 May 2025 13:56:37 -0400
Subject: [PATCH] Fix completion-ignore-case with completion--file-name-table

509cbe1c35b3d "Improve env var handling in read-file-name"
caused try-completion and all-completion operations with
completion--file-name-table to no longer update the case of text
which was already present in the input string.  That is,
completions would be returned ignoring case, but the completions
would have letter-casing which matched the input string rather
than matching the actual file names.

Fix this by always using text from the file name completions
whenever it might have changed case.  Also, fix a related bug
where mixing completion-ignore-case and expanding environment
variables would cause `read-file-name' to return the wrong
result; do this by suppressing completion-ignore-case in the
problematic case.

* lisp/minibuffer.el (completion--file-name-table): Use text
from the completions, not the input string.  (bug#78323)
* test/lisp/minibuffer-tests.el (completion-table-test-quoting):
Test with completion-ignore-case as well.
---
 lisp/minibuffer.el            | 28 +++++++++++++++++++---------
 test/lisp/minibuffer-tests.el | 16 +++++++++++++++-
 2 files changed, 34 insertions(+), 10 deletions(-)

diff --git a/lisp/minibuffer.el b/lisp/minibuffer.el
index 7b2b986aa1d..6f9e6c67541 100644
--- a/lisp/minibuffer.el
+++ b/lisp/minibuffer.el
@@ -3597,6 +3597,16 @@ completion--file-name-table
     (if (eq (car-safe action) 'boundaries)
         (cons 'boundaries (completion--sifn-boundaries orig table pred (cdr action)))
       (let* ((sifned (substitute-in-file-name orig))
+             (orig-start (car (completion--sifn-boundaries orig table pred "")))
+             (sifned-start (car (completion-boundaries sifned table pred "")))
+             (orig-in-bounds (substring orig orig-start))
+             (sifned-in-bounds (substring sifned sifned-start))
+             (only-need-double-dollars
+              ;; If true, sifn only un-doubled $s in ORIG, so we can fix a
+              ;; completion to match ORIG by just doubling $s again.  This
+              ;; preserves more text from the completion, behaving better with
+              ;; non-nil `completion-ignore-case'.
+              (string-equal orig-in-bounds (minibuffer--double-dollars sifned-in-bounds)))
              (result
               (let ((completion-regexp-list
                      ;; Regexps are matched against the real file names after
@@ -3611,21 +3621,21 @@ completion--file-name-table
           (if (stringp result)
               ;; Extract the newly added text, quote any dollar signs, and
               ;; append it to ORIG.
-              (let ((new-text (substring result (length sifned))))
-                (concat orig (minibuffer--double-dollars new-text)))
+              (if only-need-double-dollars
+                  (concat (substring orig nil orig-start)
+                          (minibuffer--double-dollars (substring result sifned-start)))
+                (let ((new-text (substring result (length sifned))))
+                  (concat orig (minibuffer--double-dollars new-text))))
             result))
          ((eq action t)                 ; all-completions
           (mapcar
-           (let ((orig-prefix
-                  (substring orig (car (completion--sifn-boundaries orig table pred ""))))
-                 (sifned-prefix-length
-                  (- (length sifned)
-                     (car (completion-boundaries sifned table pred "")))))
+           (if only-need-double-dollars
+               #'minibuffer--double-dollars
              ;; Extract the newly added text, quote any dollar signs, and append
              ;; it to the part of ORIG inside the completion boundaries.
              (lambda (compl)
-               (let ((new-text (substring compl sifned-prefix-length)))
-                 (concat orig-prefix (minibuffer--double-dollars new-text)))))
+               (let ((new-text (substring compl (length sifned-in-bounds))))
+                 (concat orig-in-bounds (minibuffer--double-dollars new-text)))))
            result))
          (t result))))))
 
diff --git a/test/lisp/minibuffer-tests.el b/test/lisp/minibuffer-tests.el
index bed797bdb14..111335a2b5e 100644
--- a/test/lisp/minibuffer-tests.el
+++ b/test/lisp/minibuffer-tests.el
@@ -108,7 +108,21 @@ completion-table-test-quoting
       (should (equal (completion-try-completion input
                                                 #'completion--file-name-table
                                                 nil (length input))
-                     (cons output (length output)))))))
+                     (cons output (length output)))))
+    ;; Everything also works with `completion-ignore-case'.
+    (let ((completion-ignore-case t))
+      (pcase-dolist (`(,input ,output)
+                     '(
+                       ("data/M-CTTQ" "data/minibuffer-test-cttq$$tion")
+                       ("data/M-CTTQ$$t" "data/minibuffer-test-cttq$$tion")
+                       ("lisp/c${CTTQ1}et/SE-U" "lisp/c${CTTQ1}et/semantic-utest")
+                       ("lisp/ced${CTTQ2}SE-U" "lisp/ced${CTTQ2}SEmantic-utest")
+                       ("lis/c${CTTQ1}/SE-U" "lisp/c${CTTQ1}et/semantic-utest")
+                       ))
+        (should (equal (car (completion-try-completion input
+                                                       #'completion--file-name-table
+                                                       nil (length input)))
+                       output))))))
 
 (ert-deftest completion--insert-strings-faces ()
   (with-temp-buffer
-- 
2.39.3