Closed karthink closed 3 years ago
(minibuffer-contents)
. If the contents change, call fish again.marginalia-classifiers
. You may have to write your own classifier detecting that this is a fish completion, since the prompt "Completion:" is too generic. Otherwise the marginalia prompt classifier would have worked.EDIT 2: Please ignore this post. The next one has my tested code that almost works.
Thank you.
2 is easy, although still untested:
(defun marginalia-classify-fish-completion ()
"Classify completion-category as `fish-completion'."
(and (eq marginalia--this-command completion-in-region-function)
(or (eq major-mode 'eshell-mode)
(eq major-mode 'shell-mode))
fish-completion-mode
'fish-completion))
(add-hook 'marginalia-classifiers 'marginalia-classify-fish-completion)
This will fail when fish-completion calls pcomplete-dirs-or-entries
to complete files, but I'm hoping one of the earlier clauses in marginalia--completion-metadata-get
will set the category to 'file.
However I'm not sure about how to do 1:
(add-to-list 'marginalia-annotator-registry
'(fish-completion marginalia-annotate-fish-completion builtin none))
(defun marginalia-annotate-fish-completion (cand)
"Annotate fish completion CAND with description."
(when-let ((desc (alist-get cand all-fish-completions-with-docstrings)))
(marginalia--field (format "%s" desc))))
How do I set and update all-fish-completions-with-docstrings
depending on (minibuffer-contents)
?
EDIT: Actually, I don't need to call out to fish at all when filtering candidates by typing in the minibuffer. I just need all-fish-completions-with-docstrings
to be set once per invocation of the completion-in-region-function
. So calling fish based on (minibuffer-contents)
seems excessive.
OK, I almost have it figured out, but it's still failing to annotate.
Make Marginalia recognize shell command argument completions as the category fish-completion
:
(defun marginalia-classify-fish-completion ()
"Classify completion-category as `fish-completion'."
(and (eq this-command 'completion-at-point)
(or (eq major-mode 'eshell-mode)
(eq major-mode 'shell-mode))
fish-completion-mode
'fish-completion))
(add-hook 'marginalia-classifiers 'marginalia-classify-fish-completion)
I have tested that this works. It's actually too general, it considers all completion function calls in eshell as in this category, including when completing elisp. I'll have to figure out how to be more specific later.
Store the results of calling out to fish for completions every time I run completion-at-point
. I define a new completion-in-region function that does this. This is also too general and has false positives, but I've tested that it works for now:
(defun my/consult-completion-in-region (start end collection &optional predicate)
"See `consult-completion-in-region'."
(let* ((initial (buffer-substring-no-properties start end))
(metadata (completion-metadata initial collection predicate))
(category (completion-metadata-get metadata 'category)))
(when (and (bound-and-true-p marginalia-mode)
(bound-and-true-p fish-completion-mode)
(eq category 'fish-completion))
(setq my/all-fish-completions-with-desc
(mapcar (lambda (e) (split-string e "\t"))
(split-string
(fish-completion--list-completions-with-desc
(buffer-substring-no-properties
(save-excursion (if (eq major-mode 'shell-mode)
(comint-bol)
(eshell-bol))
(point))
(point)))
"\n" t)))))
(consult-completion-in-region start end collection predicate))
(setq-local completion-in-region-function #'my/consult-completion-in-region)
The results from fish are stored in my/all-fish-completions-with-desc
.
Make Marginalia apply annotations for this category. This is the step that's failing:
(defun marginalia-annotate-fish-completion (cand)
"Annotate fish completion CAND with description."
(when-let ((desc (alist-get cand my/all-fish-completions-with-desc nil nil 'equal)))
(marginalia--field (car desc) :truncate 40)))
(add-to-list 'marginalia-annotator-registry
'(fish-completion marginalia-annotate-fish-completion builtin none))
I edebugged marginalia-annotate-fish-completion
, but it's never called, even when the category
is recognized as fish-completion
.
Any idea what I'm doing wrong?
2 seems too complicated. Why not strategically place advices somewhere sniffing the fish completion results? Maybe another annotator is preferred regarding 3? Look into marginalia--completion-metadata-get.
marginalia--completion-metadata-get
returns fish-completion
as expected. It calls marginalia-classify-fish-completion
a total of four times per invocation of complete-at-point
though, I don't know why. Still don't understand why marginalia-annotate-fish-completion
is never called.
Re: 2. I can probably advise one of the fish-completion functions to do this, yeah. That's a good idea.
Does marginalia--completion-metadata-get
called with the affixation-function
or the annotation-function
argument? You can check what the function returns in this case. Furthermore you can check the vertico--affixate
function, where the annotation function lookup happens, what ends up there?
You are probably hitting these problems since you are using the minibuffer completion for completion in region (consult-completion-in-region
) in combination with Marginalia. This is a use case I have not tried before.
Does marginalia--completion-metadata-get called with the affixation-function or the annotation-function argument?
This function is called several times for each invocation of consult-completion-in-region
. The first three calls are with prop set to category
, and the function returns fish-completion
each time. Then with prop set to group-function
, then affixation-function
and annotation-function
in sequence.
The affixation-function
clause has this code:
(when-let* ((cat (completion-metadata-get metadata 'category))
(annotate (marginalia--annotator cat)))
(lambda (cands)
(marginalia--context metadata
(mapcar (lambda (x) (list x "" (or (funcall annotate x) ""))) cands))))
This call to completion-metadata-get
returns nil, and thus annotate
is not set to the right annotation function. Ditto for the call to completion-metadata-get
in the annotation-function
clause.
These last two calls seem to be from vertico-affixate
. Since the annotation function is not being looked up correctly, I'm not seeing any in the result.
This call to completion-metadata-get returns nil, and thus annotate is not set to the right annotation function. Ditto for the call to completion-metadata-get in the annotation-function clause.
So you say, in the calls before the category is returned correctly as fish-completion. But in this call completion-metadata-get
returns nil? That's odd. marginalia--completion-metadata-get
advices completion-metadata-get
, so you should also see this call ending up in marginalia--completion-metadata-get
. Are the functions called with different values for metadata
?
Here are the calls I could trace, in order from the top:
(consult-completion-in-region args)
(marginalia--completion-metadata-get (metadata (completion--unquote-requote . t)) 'category)
, result fish-completion
.(marginalia--completion-metadata-get (metadata (completion--unquote-requote . t)) 'category)
, result fish-completion
.(marginalia--completion-metadata-get (metadata (completion--unquote-requote . t)) 'completion--unquote-requote)
, result nil
.(marginalia--completion-metadata-get (metadata (completion--unquote-requote . t)) 'category)
, result fish-completion
.(marginalia--completion-metadata-get (metadata (completion--unquote-requote . t)) 'category)
, result fish-completion
.(marginalia--completion-metadata-get (metadata (completion--unquote-requote . t)) 'group-function)
, result nil
.(vertico-affixate (metadata (completion--unquote-requote . t)) nil)
, --(marginalia--completion-metadata-get (metadata (completion--unquote-requote . t)) 'affixation-function)
, result nil
.----(marginalia--completion-metadata-get (metadata (completion--unquote-requote . t)) 'category)
, result nil
(NOT fish-completion
).--(marginalia--completion-metadata-get (metadata (completion--unquote-requote . t)) 'affixation-function)
, result nil
.--(marginalia--completion-metadata-get (metadata (completion--unquote-requote . t)) 'annotation-function))
, result nil
----(marginalia--completion-metadata-get (metadata (completion--unquote-requote . t)) 'category)
, result nil
(NOT fish-completion
).Sorry about the indentation. I tried to manually show which function called what, but this is incomplete. I don't know how to generate an actual stacktrace with debug-on-entry in Emacs yet.
Every call to marginalia--completion-metadata-get
seems to be with the same value of metadata
. So I'm not sure what to make of it.
Your classifier is wrong. It checks the major-mode, but the classifier is called in the mini buffer. There it will pick up the wrong mode.
Ah, that makes sense. Checking if the var fish-completion-mode
is t
doesn't work either, so it looks like I need a different signature for fish-completions that will work from the minibuffer.
OK, some success!
Here is the marginalia-classify-fish-completion
that I'm using now:
(defun marginalia-classify-fish-completion ()
"Classify completion-category as `fish-completion'."
(and (eq this-command 'completion-at-point)
(with-current-buffer (funcall (if (minibufferp) #'cadr #'car) (buffer-list))
(and fish-completion-mode
(or (eq major-mode 'eshell-mode)
(eq major-mode 'shell-mode))))
'fish-completion))
However the annotations disappear as soon as I type something:
They also disappear as soon as I scroll in vertico (without typing anything):
I'm guessing this is because marginalia-annotate-fish-completion
is called (on each candidate) every time the minibuffer contents are updated? How would I keep the annotations persistent when filtering candidates? I don't need to call fish completions again since the filtered list can only be a subset of the original results, right?
Btw why does this work in helm? Is there special support? I think that this should work out of the box with consult+vertico. I will install fish later and try this.
Edit: it seems there is a helm-fish-completion package. Do you use that?
Edit2: I think it would be significantly easier and more robust if you add an annotation-function to the fish-completion package directly.
This snippet works for me:
(setq completion-in-region-function #'consult-completion-in-region)
(defvar last-fish-completions nil)
(defun add-fish-annotations (result)
`(,@result :annotation-function fish-annotate))
(defun fish-annotate (cand)
(when-let (ann (cdr (assoc cand last-fish-completions)))
(concat (propertize " " 'display '(space :align-to center)) ann)))
(defun my-fish-completion--list-completions (raw-prompt)
(setq last-fish-completions
(mapcar (lambda (e)
(string-match "\\`\\([^\t]*\\)\t?\\(.*\\)\\'" e)
(cons (match-string 1 e) (match-string 2 e)))
(split-string
(fish-completion--list-completions-with-desc raw-prompt)
"\n" t)))
(mapcar #'car last-fish-completions))
(advice-add #'pcomplete-completions-at-point :filter-return #'add-fish-annotations)
(advice-add #'fish-completion--list-completions :override #'my-fish-completion--list-completions)
Pcomplete does not seem to offer a possibility to supply annotations unfortunately, therefore I am adding the :annotation-function
to the return value of pcomplete-completions-at-point
, which is the pcomplete capf function. The resulting solution here is certainly not very clean, but at least we don't have to add Marginalia to the equation. However it is questionable if this solution can be integrated with upstream fish-completion, since it breaks the pcomplete abstraction layer by adding the annotations.
Some background: We are going to a multitude of completion layers here.
If you use Corfu instead, this would replace layer 6+7. The pcomplete layer 3 seems to be mostly legacy since it just sits between the capf layer and the actual completion backend, but I am not entirely sure. Layer 3 is the one which does not support annotations out of the box.
Here is a better variant without global state. My suggestion is to propose this as a patch to the fish-completion
package. The fish-completion--provide-annotation-function
advice should only be installed when the fish-completion-mode
is active.
(defun fish-completion--list-completions (raw-prompt)
(mapcar (lambda (e)
(string-match "\\`\\([^\t]*\\)\t?\\(.*\\)\\'" e)
(propertize (match-string 1 e) 'fish-completion--annotation (match-string 2 e)))
(split-string
(fish-completion--list-completions-with-desc raw-prompt)
"\n" t)))
(defun fish-completion--annotate (cand)
(when-let* ((pos (next-single-property-change 0 'fish-completion--annotation cand))
(ann (get-text-property pos 'fish-completion--annotation cand)))
(concat (propertize " " 'display '(space :align-to center)) ann)))
(defun fish-completion--provide-annotation-function (table)
(nconc table (list :annotation-function #'fish-completion--annotate)))
(advice-add #'pcomplete-completions-at-point :filter-return #'fish-completion--provide-annotation-function)
I tried this but I don't see the annotations. Is the property fish-completion--annotation
supposed to be a face? It's not defined in your code above or in fish-completions.el.
EDIT: I debugged fish-completion--annotate
and while it's being called on all the completion candidates, (next-single-property-change 0 'fish-completion--annotation cand)
is returning nil
.
Some background: We are going to a multitude of completion layers here.
Yeah, this is part of the reason I got very confused trying to set this up.
Edit: it seems there is a helm-fish-completion package. Do you use that?
Yeah, I used helm-fish-completions
to get the docstrings in helm. Sorry, I didn't go into the details since it wasn't relevant for marginalia. This package defines helm-esh-pcomplete
that works as a drop-in replacement for completion-at-point
. I guess this is in keeping with the helm way of replacing chunks of Emacs instead of adding to them.
Edit2: I think it would be significantly easier and more robust if you add an annotation-function to the fish-completion package directly.
I see that from your code. My hack was really fragile. Thanks, I think I understand how marginalia/annotations work a little better now.
Both these snippets work for me on 27.1 and there is not much that can go wrong. Try with emacs -Q step by step. fish-completion--annotation is a text property I introduced here. I suggest you ask on the fish-completion package to add something like this:
https://github.com/minad/marginalia/issues/87#issuecomment-880724829
Closing here since it is better to solve this on the level of fish-completion directly instead of going via Marginalia.
I suggest you ask on the fish-completion package to add something like this:
I'll do that, thank you.
By the way, your code above fails when completion-ignore-case
set to t
. Setting it to nil
makes it work fine again. I tested this with emacs -Q
.
It took a while to track down, but this is why it wasn't working for me. Here are the propertized candidates being passed to the annotation function when
completion-ignore-case
is nil
)#("--group-directories-first" 0 2 (face (orderless-match-face-0 completions-common-part)) 2 25 (fish-completion--annotation "Group directories before files"))
completion-ignore-case
is t
)#("--group-directories-first" 0 2 (face (orderless-match-face-0 completions-common-part) fish-completion--annotation #1="Group directories before files") 2 25 (fish-completion--annotation #1#))
That's odd. Why does this happen?
I can't figure out why this happens, but what's happening is clear. In one case the text property fish-completion--annotation
you defined is applied to the entire candidate, in the other it's applied to the non-common part of each candidate. I got around it by modifying fish-completion--annotate
:
(defun fish-completion--annotate (cand)
(when-let* ((pos (or (next-single-property-change 0 'fish-completion--annotation cand)
0)
(ann (get-text-property pos 'fish-completion--annotation cand)))
(concat (propertize " " 'display '(space :align-to center)) ann)))
Also, pcomplete-completions-at-point
is going to be advised globally whenever fish-completion-mode is active. Does this mean if a different comint mode (say) is using it, it will get a completion table with the :annotation-function #'fish-completion--annotate
plist element appended? Is there some way of setting this up to only modify pcomplete-completions-at-point
in the eshell/shell buffer?
Hi @minad and @karthink I have spent some time doing this for the doom eshell module and building on what you have above I think I have a pretty workable solution for this in the form of a capf.
The whole code is this,
(defun +eshell-fish-completion-list (raw-prompt)
"Return list of completion candidates for RAW-PROMPT."
(mapcar (lambda (e) (let ((res (split-string e "\t")))
(propertize (car res) 'fish-annotation (cadr res))))
(split-string
(fish-completion--list-completions-with-desc raw-prompt)
"\n" t)))
(defun +eshell-fish-capf ()
"A a capf for fish-completion."
(when-let ((args (ignore-errors (eshell-complete-parse-arguments)))
(table (+eshell-fish-completion-list (buffer-substring (cadr args) (point))))
((not (file-exists-p (car table)))))
(list (car (last args)) (point) table
:exclusive 'no
:annotation-function #'+eshell-fish-completion-annotate)))
(defun +eshell-fish-completion-annotate (cand)
(when-let* ((pos (or (next-single-property-change 0 'fish-annotation cand)
0))
(ann (get-text-property pos 'fish-annotation cand)))
(concat (propertize " " 'display '(space :align-to center)) ann)))
(defun +eshell-use-annotated-completions-h ()
"Use annotaed fish completions."
(if fish-completion-mode
(add-hook 'completion-at-point-functions #'+eshell-fish-capf nil t)
(remove-hook 'completion-at-point-functions #'+eshell-fish-capf t)))
(add-hook 'fish-completion-mode-hook #'+eshell-use-annotated-completions-h)
The only dependency on fish-completions
is actually a call to fish -c complete -C some-string
and a lot of what it has deal with is replaced by the call eshell-complete-parse-arguments
which also takes care of things like subcommand completions.
The one weird thing about eshell-complete-parse-arguments
is that if there is a sexp behind point, it tries to evaluate it and that can be potentially very time consuming. The pcomplete
mechanism already calls this function so has this problem but now it can happen twice. I am not a heavy shell user so I don't know if that evaluation is useful for something or not. And it is entirely possible that I am missing problems with this approach for the same reason.
On 8/13/22 22:46, aikrahguzar wrote:
Hi @minad https://github.com/minad and @karthink https://github.com/karthink I have spent some time doing this for the doom eshell module and building on what you have above I think I have a pretty workable solution for this in the form of a capf.
I've also considered using a Capf. Unfortunately the Capf won't integrate well with Pcomplete. Instead it will take over completely.
However I am not using fish completion anyway. I switched to (my personal clone of) pcmpl-args, which parses --help output. I am not fond of the idea of shelling out to fish, when we can do the same in Elisp.
I've also considered using a Capf. Unfortunately the Capf won't integrate well with Pcomplete. Instead it will take over completely.
There is the inversion of precedence between completions provided by fish and those by Pcomplete but it mostly that seems like an improvement for most use cases. Getting annotations for command names was difficult but doable but even more difficult was getting completions for subcommands. Pcomplete tries to complete a filename most of the time in that situation while fish does the right thing most of the time. This is the most likely due to people having written more completions for fish. For file and elisp completions the capf above doesn't activate so the pcomplete capf takes over.
However I am not using fish completion anyway. I switched to (my personal clone of) pcmpl-args, which parses --help output. I am not fond of the idea of shelling out to fish, when we can do the same in Elisp.
Can the annotation coming from parsing --help be included in the normal Pcomplete completions using advice?
By the way fish does something similar. There is fish command fish_update_completions
which parses the manual pages to create completions. The results for CMD are in ~/.local/share/fish/generated_completions/CMD
for me. For example the first line of ~/.local/share/fish/generated_completions/emacsclient
is,
complete -c emacsclient -s a -l alternate-editor -d 'If the Emacs server is not running, run the specified shell command instead'
Each line contains a similar completion. Completions that come from system packages are in /usr/share/fish/completions
and /usr/share/fish/vendor_completions.d
for me. So another options which doesn't involve shelling would be to parse these files once to generate completions for a capf (which doesn't seem too hard but not trivial either) or pcomplete (is there a way other than defining a pcomplete/CMD for each of these?).
I'm trying to write a function
marginala-annotate-fish-completion
that works with the fish completion package to provide fish "docstrings" as annotations in eshell/shell. To do this I turn onfish-completion-mode
and setcompletion-in-region-function
toconsult-completion-in-region
in eshell/shell buffers.Here is an example of the docstrings showing up along with the completions in Helm:
And here is how it shows up with
consult-completion-in-region
:I'm facing two issues:
fish-completion--list-completions-with-desc
function that provides an alist of completions with their docstrings when given a string to complete on:(Technically it returns a string, but I converted it to an alist here for convenience.) However it looks like the hypothetical
marginalia-annotate-fish-completion
will be called on each completion candidate separately, /i.e/ once on "--version", once on "--help" etc. Where do I store this list in relation to marginalia so I can just look up each docstring from it?pcomplete-default-completion-function
:However it's not setting the completion category when it completes command line arguments, it evaluates to nil. So there's nothing for marginala to match against. When the completion candidates are files,
fish-completion-shell-complete
callspcomplete-dirs-or-entries
instead, and this does set the category as 'file. Where/How do I set the category for CLI arguments?