Subject: bug#66187: read-file-name unexpected behavior when MUSTMATCH is a function

From: Michael Heerdegen <[email-masked]>
Date: Wed, 27 Sep 2023 03:43:25 +0200

Joseph Turner <[email-masked]> writes:

> > Yes, looks like a bug that this is not possible.  It should be possible
> > to complete directory names that do not match.
>
> I'm happy to work on the bug, if others agree that this change is
> desired.

I had a look and tried this:

From ad895d2df5c69e015c2c7eba5116ab2d440e1fbc Mon Sep 17 00:00:00 2001
From: Michael Heerdegen <[email-masked]>
Date: Wed, 27 Sep 2023 03:32:37 +0200
Subject: [PATCH] WIP: Bug#66187

---
 lisp/minibuffer.el | 29 +++++++++++++++--------------
 1 file changed, 15 insertions(+), 14 deletions(-)

diff --git a/lisp/minibuffer.el b/lisp/minibuffer.el
index 2120e31775e..79a8786fbac 100644
--- a/lisp/minibuffer.el
+++ b/lisp/minibuffer.el
@@ -3061,13 +3061,23 @@ completion-file-name-table
           (funcall (or pred 'file-exists-p) string)))

        (t
-        (let* ((name (file-name-nondirectory string))
+        (let* ((test-directory (lambda (s)
+                                 (let ((len (length s)))
+                                   (and (> len 0) (eq (aref s (1- len)) ?/)))))
+               (should-complete
+                (and pred
+                     (if (eq pred 'file-directory-p)
+                         test-directory
+                       (lambda (f)
+                         (or (funcall test-directory f)
+                             (funcall pred f))))))
+               (name (file-name-nondirectory string))
                (specdir (file-name-directory string))
                (realdir (or specdir default-directory)))

           (cond
            ((null action)
-            (let ((comp (file-name-completion name realdir pred)))
+            (let ((comp (file-name-completion name realdir should-complete)))
               (if (stringp comp)
                   (concat specdir comp)
                 comp)))
@@ -3078,18 +3088,9 @@ completion-file-name-table
               ;; Check the predicate, if necessary.
               (unless (memq pred '(nil file-exists-p))
                 (let ((comp ())
-                      (pred
-                       (if (eq pred 'file-directory-p)
-                           ;; Brute-force speed up for directory checking:
-                           ;; Discard strings which don't end in a slash.
-                           (lambda (s)
-                             (let ((len (length s)))
-                               (and (> len 0) (eq (aref s (1- len)) ?/))))
-                         ;; Must do it the hard (and slow) way.
-                         pred)))
-                  (let ((default-directory (expand-file-name realdir)))
-                    (dolist (tem all)
-                      (if (funcall pred tem) (push tem comp))))
+                      (default-directory (expand-file-name realdir)))
+                  (dolist (tem all)
+                    (if (funcall should-complete tem) (push tem comp)))
                   (setq all (nreverse comp))))

               all))))))
--
2.39.2

I thought this would make Emacs complete as you want.  But: what files
should be shown when hitting TAB?  I decided to show only matching
files, plus all directories (seems logical, since these may contain
matching files).

But that gives a bad user experience: When I tried this I thought it
would not work because completion did not accept an existing empty
directory.  Then I saw that it actually was not empty.  But it contained only
plain files, no subdirectories, and TAB displayed nothing.  Quite confusing!

> How about the attached patch?

| diff --git a/lisp/files.el b/lisp/files.el
| index b72f141c0ee..1fe124848b9 100644
| --- a/lisp/files.el
| +++ b/lisp/files.el
| @@ -817,7 +817,7 @@ non-empty string that was inserted by this function.
|  If the user exits with an empty minibuffer, this function returns
|  an empty string.  (This can happen only if the user erased the
|  pre-inserted contents or if `insert-default-directory' is nil.)
| -Fourth arg MUSTMATCH non-nil means require existing directory's name.
| +Fourth arg MUSTMATCH is passed as-is to `read-file-name', which see.

Ok from my side.

What do others think about all of this?

Thx,

Michael.