5

Can someone please tell me how can I force completing-read to search not only in the displayed rows, but also within annotations?

Consider a trivial example such as this:

(let* ((coll '(("Affenpinscher" . "Loyal and amusing")
               ("Akita" . "Ancient Japanese")
               ("Bulldog" . "Kind but courageous")
               ("Caucasian Shepherd" . "Serious guarding breed")
               ("Miniature Schnauzer" . "Long-lived and low-shedding")))
       (completion-extra-properties
        '(:annotation-function
          (lambda (k)
            (let ((desc
                   (alist-get
                    k minibuffer-completion-table
                    nil nil #'string=)))
              (format "\n\t%s" desc))))))
  (completing-read "Select a breed: " coll))

Evaling this snippet would display names of the dog breeds, along with annotated descriptions. And if I type sheph, it would correctly narrow the search to "Caucasian Shepherd". But I also want to be able to type "long" and to find "Miniature Schnauzer", but sadly, it doesn't work that way. How can I change that?

iLemming
  • 1,223
  • 9
  • 14
  • https://emacs.stackexchange.com/tags/elisp/info – Drew Nov 13 '22 at 02:31
  • As far as I know, you can't match annotation text. But maybe this has changed, or maybe there's a 3rd-party library that lets you do that. (You could of course move the annotation text to be part of the completion-candidate strings...) – Drew Nov 13 '22 at 02:34
  • Turns out, not only you can but there's more... see my answer – iLemming Nov 13 '22 at 03:26

2 Answers2

4

It looks like this can be done via a completing-read collection function, https://www.gnu.org/software/emacs/manual/html_node/elisp/Programmed-Completion.html

Some basic, dirty experimentation got me to something like this:

(setq lexical-binding t)

(defun dogs-filter (seq)
  (lambda (str pred flag)
    (pcase flag
      ('metadata
       (list 'metadata
             (cons 'annotation-function
                   (lambda (c)
                     (format "\n\t%s" (alist-get c seq nil nil #'string=))))))
      ('t
       (if (string-blank-p str)
           (all-completions str seq)
         (all-completions
          str
          (lambda (s _ _)
            (seq-map
             #'car
             (seq-filter
              (lambda (x)
                (unless (string-blank-p str)
                  (or
                   (s-contains-p str (car x) :ignore-case)
                   (s-contains-p str (cdr x) :ignore-case))))
              seq)))))))))

(let* ((coll '(("Affenpinscher" .  "Loyal and amusing")
               ("Akita" . "Ancient Japanese")
               ("Bulldog" . "Kind but courageous")
               ("Caucasian Shepherd" . "Serious guarding breed")
               ("Miniature Schnauzer" . "Long-lived and low-shedding"))))

  (completing-read
   "Select a breed: "
   (dogs-filter coll)))

There's some weird case I can't seem to solve at the moment inside seq-filter, that's why I had to do that weird "if" check, otherwise it won't show the rows unless something typed.

But it works as I wanted. With the completion-fn, I no longer have to use completion-extra-properties, they can be attached via metadata right in that function.

Doing that way you can also attach metadata that allows you to set the category (e.g., file, url, etc.), grouping, annotations, affixation, and more. Setting a proper category, for example for URLs (if you're dealing with those) would correctly dispatch Embark actions.

Unfortunately, the documentation is not very clear and is lacking examples, it's a bit tricky to get it right, but that's the gist of how it gets done.

Another great example of using a collection function I've found in this blogpost: https://kisaragi-hiu.com/emacs-completion-metadata.html


In this Reddit post where I asked the same exact question, a lot of opinions were like: "you shouldn't be doing that, annotations are not made for that", and frankly, I think they are missing the point. In this comment, I tried to explain why Programmed Completions are so powerful and that they can be used not only to match what's in annotations but for cases when you want to match on something that practically, can't be displayed at all.

I'd recommend learning this powerful tool and don't let the ascetic language of the official documentation keep you from building some awesome things in elisp.

iLemming
  • 1,223
  • 9
  • 14
2

EDIT

Although this answer, almost achieves what you want (it shows completions when string is empty, and it lets you complete on breed and annotations), it unfortunately errors in some/many cases because the there is no completion-style that supports showing alternatives that do not match at all. For example, when typing "A", "Bulldog" is returned as completion candidate because its annotation contains an "A". However, the is no completion-style that supports showing the Bulldog alternative because it does not match the input in any way. It does complete to Bulldog when typing "ag" though, because then Bulldog is the only alternative, and completion-styles does not have to choose present completion alternatives. I am not sure how to solve this one small/big annoying error (except for defining another completion style).

Except for that annoying error, this solution provides the functionality you ask for in the 'idiomatic' way.

END EDIT

You can read about how to use a function as collection here (I think you've found that already). I guess the following should achieve what you want (it completes also on annotations, but to show the annotations in the offered completions you should somehow put them back):

(completing-read
 "Select a breed: "
 (lambda (s pred flag)
   (let ((collection '(("Affenpinscher" .  "Loyal and amusing")
               ("Akita" . "Ancient Japanese")
               ("Bulldog" . "Kind but courageous")
               ("Caucasian Shepherd" . "Serious guarding breed")
               ("Miniature Schnauzer" . "Long-lived and low-shedding"))))
     (pcase flag
       ('t (mapcar #'car
           (seq-filter
            (lambda (x)
              (or (string-match-p s (car x))
              (string-match-p s (cdr x))))
            collection)))
       ('metadata (list 'metadata
            (cons 'annotation-function
                  (lambda (c)
                (concat " " (alist-get c collection nil nil #'string=))))))))))
dalanicolai
  • 6,108
  • 7
  • 23
  • 1
    Thank you. Although your solution doesn't work. It's tricky to get the filter function right (as I noted in my answer), but yes, this is the gist of the idea how something like this could be done. I had no idea how powerful "programmed completion" feature is. I wish the documentation explained it better. – iLemming Nov 13 '22 at 20:35