Subject: Re: bug#61283: 29.0.60; pcomplete-completions-at-point loses text properties, breaking pcomplete-from-help annotations

From: Jim Porter <[email-masked]>
Date: Mon, 22 May 2023 21:16:59 -0700

On 5/18/2023 6:30 AM, Stefan Monnier via Bug reports for GNU Emacs, the 
Swiss army knife of text editors wrote:
>> ... since getting a 100% reliable solution here might take quite a bit more
>> work than I thought, how about a not-quite-100% hack? Instead of
>> propertizing the first character of each candidate, propertize the *last*
>> character (or the entire string?). That won't get thrown out quite
>> so easily.
> 
> I'll see your "last char" and raise you a "the whole string" (and then
> use the property on the first char where it's found, using
> `next-single-property-change`)?

After thinking this over some more, how about a completely different 
strategy like the following? This uses a programmed completion 
function[1] to supply an annotation-function as appropriate.

This also fixes a tangentially-related issue where long options that 
take an argument added an extraneous space after the trailing "=". (This 
bit is actually the original patch I was working on before discovering 
the issue described in this bug.)

Pcomplete might have some issues with handling programmed completion 
functions like this, but I *think* it makes sense to go this route if 
possible: making Pcomplete work more like the rest of Emacs' completion 
code would be very nice.

[1] "Programmed completion" vs "programmable completion (Pcomplete)": 
*that's* definitely not confusing at all. ;)

From 44a0833ee1891ea84727ca994ecfa1a34a49d5e3 Mon Sep 17 00:00:00 2001
From: Jim Porter <[email-masked]>
Date: Fri, 10 Feb 2023 13:52:33 -0800
Subject: [PATCH] WIP: Use a programmed completion function to handle long
 options in Pcomplete

This does two things:

1. When completing an option that takes an argument (i.e. an option
   string ending in "="), don't add a trailing space.  The space is
   unhelpful, since the user likely wants to add a value for that
   option immediately after the "=".

2. Return an appropriate 'annotation-function' for the option to
   handle the 'pcomplete-annotation' text property.  Maybe we should
   store the annotation somewhere other than a text property, but this
   works as a proof of concept...
---
 lisp/pcmpl-gnu.el |  3 ++-
 lisp/pcomplete.el | 41 ++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 42 insertions(+), 2 deletions(-)

diff --git a/lisp/pcmpl-gnu.el b/lisp/pcmpl-gnu.el
index 1553c3efed7..488a0501a48 100644
--- a/lisp/pcmpl-gnu.el
+++ b/lisp/pcmpl-gnu.el
@@ -274,7 +274,8 @@ pcomplete/tar
       (if (pcomplete-match "^--" 0)
           (cond
            ((pcomplete-match "^--\\([^= \t\n\f]*\\)\\'" 0)
-            (pcomplete-here* pcmpl-gnu--tar-long-options))
+            (pcomplete-here* (pcomplete-long-option-completion-table
+                              pcmpl-gnu--tar-long-options)))
            ((pcomplete-match "\\`--directory=\\(.*\\)" 0)
             (pcomplete-here* (pcomplete-dirs)
                              (pcomplete-match-string 1 0)))
diff --git a/lisp/pcomplete.el b/lisp/pcomplete.el
index 36f68f1af57..4850a3ab9c1 100644
--- a/lisp/pcomplete.el
+++ b/lisp/pcomplete.el
@@ -1371,6 +1371,44 @@ pcomplete-read-host-names
       (pcomplete-read-hosts pcomplete-hosts-file 'pcomplete--host-name-cache
                    'pcomplete--host-name-cache-timestamp)))
 
+(defun pcomplete-long-option-completion-table (options)
+  "Make a completion table for a list of OPTIONS.
+OPTIONS should be a list of option strings; if a string ends in
+\"=\", it's an option that accepts a value.  You can also set the
+completion annotation by putting the `pcomplete-annotation'
+property on the first character of an option."
+  (lambda (string pred action)
+    (pcase action
+      ('nil                               ; try-completion
+       (let ((result (try-completion string options pred)))
+         ;; If the completion ends in "=", then it's not really
+         ;; complete yet: we expect a value after the "=".  Return the
+         ;; original STRING in this case instead of t to indicate
+         ;; that.
+         (if (and (eq result t) (string-suffix-p "=" string))
+             string
+           result)))
+      ('t                                 ; all-completions
+       (all-completions string options pred))
+      ('lambda                            ; test-completion
+        (let ((result (test-completion string options pred)))
+          (if (and (eq result t) (string-suffix-p "=" string))
+              string
+            result)))
+      ('metadata
+       ;; FIXME: There's probably a cleaner way to do this if we
+       ;; change how OPTIONS is structured.  We should also support
+       ;; help text.
+       `(metadata (annotation-function
+                   . ,(lambda (s)
+                        (get-text-property
+                         0 'pcomplete-annotation
+                         (seq-find (lambda (cand) (string= s cand))
+                                   options))))))
+      (`(boundaries . ,_suffix)
+       ;; FIXME: Implement boundaries.
+       '(boundaries 0 . 0)))))
+
 ;;; Parsing help messages
 
 (defvar pcomplete-from-help (make-hash-table :test #'equal)
@@ -1495,7 +1533,8 @@ pcomplete-here-using-help
            (pcomplete-here (pcomplete-entries)
                            (pcomplete-match-string 1 0)))
           ((string-prefix-p "-" (pcomplete-arg 0))
-           (pcomplete-here (apply #'pcomplete-from-help command args)))
+           (pcomplete-here (pcomplete-long-option-completion-table
+                            (apply #'pcomplete-from-help command args))))
           (t (pcomplete-here* (pcomplete-entries))))))
 
 (provide 'pcomplete)
-- 
2.25.1