Subject: Re: bug#72210: 31.0.50; Feature request: multi-category support in `icomplete-fido-kill'

From: Fernando de Morais <[email-masked]>
Date: Thu, 10 Apr 2025 00:51:45 -0300

Hello Stefan,

Stefan Monnier <[email-masked]> writes:

> Any objection to the patch below?

With this new version, I can no longer specify the candidate's true
category through the prompt.  It will always show ``multi-category''
when `icomplete-fido-kill' is used in such scenarios.

I came up with a few suggestions in the patch at the end of the message.
With it, the prompt is confined to another function, which allows me to
do something like this in my configurations:

#+begin_src emacs-lisp
;; From Embark <https://elpa.gnu.org/packages/embark.html>
(defun icomplete--refine-multi-category (target)
  "Refine `multi-category' TARGET to its actual type."
  (or (let ((mc (get-text-property 0 'multi-category target)))
        (cond
         ;; The `cdr' of the `multi-category' property can be a buffer object.
         ((and (eq (car mc) 'buffer) (buffer-live-p (cdr mc)))
  	(cons 'buffer (buffer-name (cdr mc))))
         ((stringp (cdr mc)) mc)))
      (cons 'general target)))

(defun icomplete-kill-candidate--override (category candidate)
  "\"Kill\" CANDIDATE based on CATEGORY.

This function calls `icomplete-kill' to perform the operation.  Se it's
documentation for the actual meaning of \"Kill\"."
  (let* ((refined (when (eql category 'multi-category)
		    (icomplete--refine-multi-category candidate)))
         (category (or (car-safe refined) category))
         (candidate (or (cdr-safe refined) candidate)))
    (when (yes-or-no-p (format "Kill %s %s? " category candidate))
      (icomplete-kill category candidate))))

(advice-add 'icomplete-kill-candidate :override
	    #'icomplete-kill-candidate--override)
#+end_src

Such code in my init, allows me to continue using `icomplete-fido-kill'
the way I’ve been doing over the past few months.

Do you think something like what's suggested in the patch below could be
used?

Thanks.

-- 
Regards,
Fernando de Morais.

diff --git a/lisp/icomplete.el b/lisp/icomplete.el
index 35842b53e6b..2bc98a018a3 100644
--- a/lisp/icomplete.el
+++ b/lisp/icomplete.el
@@ -381,6 +381,37 @@ icomplete-vertical-goto-last
 
 ;;;_* Helpers for `fido-mode' (or `ido-mode' emulation)
 
+(cl-defgeneric icomplete-kill (category _candidate)
+  "\"Kill\" CANDIDATE, assuming it is of kind CATEGORY.
+CANDIDATE is a string denoting a completion candidate,
+CATEGORY should be a completion category, as specified
+in `completion-metadata'.
+\"Kill\" here means to actually delete the underlying object, such
+as a file, buffer, ...
+Should return non-nil if the operation was successful."
+  (error "Don't know how to kill things for category `%s'" category))
+
+(cl-defmethod icomplete-kill ((_ (eql 'buffer)) thing)
+  (kill-buffer thing))
+
+(cl-defmethod icomplete-kill ((_ (eql 'file)) thing)
+  ;; FIXME: This makes assumptions about completion style: e.g. with
+  ;; partial-completion, `/usr/s/d/ema' can result in DIR being
+  ;; `/usr/s/d/' and THING being `share/doc/emacs', in which case DIR
+  ;; isn't the right base directory to pass to `expand-file-name'!
+  (let* ((dir (file-name-directory (icomplete--field-string)))
+         (file (expand-file-name thing dir)))
+    (delete-file file)
+    t))
+
+(defun icomplete-kill-candidate (category candidate)
+  "\"Kill\" CANDIDATE based on CATEGORY.
+
+This function calls `icomplete-kill' to perform the operation.  Se it's
+documentation for the actual meaning of \"Kill\"."
+  (when (yes-or-no-p (format "Kill %s %s? " category candidate))
+    (icomplete-kill category candidate)))
+
 (defun icomplete-fido-kill ()
   "Kill line or current completion, like `ido-mode'.
 If killing to the end of line make sense, call `kill-line',
@@ -395,26 +426,12 @@ icomplete-fido-kill
         (call-interactively 'kill-line)
       (let* ((all (completion-all-sorted-completions))
              (thing (car all))
-             (cat (icomplete--category))
-             (action
-              (cl-case cat
-                (buffer
-                 (lambda ()
-                   (when (yes-or-no-p (concat "Kill buffer " thing "? "))
-                     (kill-buffer thing))))
-                ((project-file file)
-                 (lambda ()
-                   (let* ((dir (file-name-directory (icomplete--field-string)))
-                          (path (expand-file-name thing dir)))
-                     (when (yes-or-no-p (concat "Delete file " path "? "))
-                       (delete-file path) t))))
-                (t
-                 (error "Sorry, don't know how to kill things for `%s'" cat)))))
+             (cat (icomplete--category)))
         (when (let (;; Allow `yes-or-no-p' to work and don't let it
                     ;; `icomplete-exhibit' anything.
                     (enable-recursive-minibuffers t)
                     (icomplete-mode nil))
-                (funcall action))
+                (icomplete-kill-candidate cat thing))
           (completion--cache-all-sorted-completions
            (icomplete--field-beg)
            (icomplete--field-end)
@@ -437,6 +454,8 @@ icomplete-fido-ret
                    (file-name-directory (icomplete--field-string))))
          (current (car completion-all-sorted-completions))
          (probe (and dir current
+                     ;; FIXME: Same problem as in
+                     ;; `icomplete-kill-candidate<file>' above.
                      (expand-file-name (directory-file-name current)
                                        (substitute-env-vars dir)))))
     (cond ((and probe (file-directory-p probe) (not (string= current "./")))