minad / cape

🦸cape.el - Completion At Point Extensions
GNU General Public License v3.0
573 stars 20 forks source link

A new way to wrap eglot's CAPF (with some help) #105

Closed jdtsmith closed 5 months ago

jdtsmith commented 5 months ago

A discussion on emacs-devel gave me an idea for a new and better way to interact with eglot's CAPF. It's a remix of various approaches we've tried. The solution:

  1. A custom orderless style dispatcher which passes all matches unfiltered from the first orderless term (I already do this), and
  2. A cape buster wrapper which causes the CAPF to "continuously update" completions as the first term (only) gets edited, ignoring all other terms.

This combines the best of both worlds: continuous update of the starting term, and orderless based filtering "after the space" (entered in corfu as M-SPC). It's easy to know which "side" of the table-dumping just-ask-the-LSP-server fence you are on: are your edits before or after the space?

So something like this should do the trick:

(cl-pushnew
 (lambda (pattern index &rest r) (when (eq index 0) "")) ; "" = "all candidates"
     orderless-style-dispatchers)
(cl-nsubst
 (cape-capf-buster
  #'eglot-completion-at-point
  (cl-labels ((not-space (x) (/= x ?\s)))
    (lambda (old new)      ; valid if everything up to space unchanged
      (equal (seq-take-while #'not-space old)
         (seq-take-while #'not-space new)))))
 'eglot-completion-at-point
 completion-at-point-functions)

But alas, no luck. Then, in cape-wrap-buster I see:

              (unless (or (string-match-p "\\s-" new-input) ;; Support Orderless
                          (funcall valid input new-input))

Ahah, this is actively thwarting multi-orderless-by-term busting, which is exactly what I needed. So I removed that. But still no dice. Then I see:

(guard (and (= beg new-beg) (= end new-end)))

At first, I think: this must be a bug. If you are editing the search term (adding/deleting), you'd very much expect the LSP server to return a new beg and end. So I think this should be simply (guard (= beg new-beg)). But then I see you're using markers for beg and end as closure variables. Clever! But actually, too clever, when orderless terms are used. That's because end moves ahead with M-SPC, but the eglot CAPF does not consider anything past the space as relevant for completion, so if you come bak and edit the first term, beg and end are mismatched, and from then on, no matter what the valid function has to say, the capf is no longer busted.

One solution would be to ensure M-SPC leaves the end marker behind (I'm not sure why it wouldn't atm). But perhaps better is just (guard (= beg new-beg)), which is IMO safe enough.

With those in place, the experience is quite excellent. Within the first term, eglot behaves "just like company". After the first term, it behaves "just like orderless". M-SPC is the clear delineator between these two "modes of operation", and you can happily edit on "either side" and it does the right thing.

Would you consider an optional argument to cape-capf-buster to disable the orderless specific validity assertion? And what do you think about guarding only the CAPF's beg against motion?