minad / cape

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

Question: usage with buffer local CAPFs #24

Closed andreyorst closed 2 years ago

andreyorst commented 2 years ago

Hi. Thanks for a great package!

I have a small usability question, that raised when I've migrated from Company. Company has its own notion of backends, which works independently from the completion-at-point-functions variable, which is suggested to be modified when using this package. However, there are a lot of other packages, that also modify this variable, by making it buffer-local via a hook. This results in the fact that manually added capes are absent after loading such a module.

In my particular example, I care about cape-file and cape-dabbrev backends, so I add those to completion-at-point-functions with a prog-mode hook. Then, when I open, say, a Clojure file, and start a CIDER session, CIDER calls add-hook with optional argument local set to t. This changes the globally set variable to a locally set one, that contains only the backend set by CIDER. This can be migrated, if I also use buffer-local hooks, then the value keeps backend added by me, and a CIDER backend. But it doesn't work always.

Other examples involve the LSP mode package and Sly/SLIME. LSP-mode, for instance, kills every other backend and installs its own. Sly does a similar thing. I understand, that both LSP mode and Sly are trying to provide IDE-like completion experience, but this makes it harder to set cape backends reliably. Company-mode avoided this by using its own notion of backends, though I do understand that this is out of the scope of Cape and Corfu.

This describes the situation. I'm juggling between mode-specific hooks to automatically add needed backends, and default local hooks for all modes that don't modify the variable. This works, but a bit clunky. Any suggestions, on better management of cape backends?

minad commented 2 years ago

We don't have to look so far, this even applies to all the usual builtins like emacs-lisp-mode. There the completion-at-point-functions has the buffer-local value (elisp-completion-at-point t). This mean that first elisp-completion-at-point is tried and due to the t the global completion-at-point-functions are tried afterwards. If we register the cape-* capfs globally then they are tried afterwards. Therefore in elisp mode the cape-capfs only trigger when completing within comments or string literals. This is the intended behavior.

If packages override the completion-at-point-functions locally in a different way, e.g., by setting it to only (cider-completion-at-point) you have to fix this on a case by case basis by adding a function to the respective *-mode-hook.

(add-hook 'cider-mode-hook
  (lambda () (setq-local completion-at-point-functions '(cider-completion-at-point t))))

Furthermore sometimes Capfs are too aggressive and always take over even if they don't return candidates. Then one should mark the Capf as non-exclusive. In my opinion almost all Capfs should be non-exclusive (all Cape-Capfs are non exclusive) but in reality almost no Capf is non-exclusive. We can also modify this easily:

;; Alternative 1: Create lambda wrapper
(add-hook 'cider-mode-hook
  (lambda ()
    (setq-local completion-at-point-functions 
      (list (cape-capf-properties #'cider-completion-at-point :exclusive 'no) t)))

;; Alternative 2: Use advice wrapper
(advice-add #'cider-completion-at-point :around
  (lambda (orig-fun) (cape-wrap-properties orig-fun :exclusive 'no)))

So overall we have a lot of flexibility and all these issues can be solved, but it requires configuration on a case by case basis. Ideally we document such setups in the Corfu wiki, to help other users who want to migrate. By documenting multiple such setups we show the common pattern. I would appreciate if you contribute there if you figure out a good setup which works for you with Slime/Cider etc. Does this answer your question?

andreyorst commented 2 years ago

Does this answer your question?

Yes, thanks for the detailed answer.

This mean that first elisp-completion-at-point is tried and due to the t the global completion-at-point-functions are tried afterwards.

I wasn't aware of this. Perhaps because nothing usually sits in the global completion-at-point-functions in my setup, except for the default tags-completion-at-point-function which never worked, as I don't use tags. I guess, when I've tried setting cape capfs as a global value it didn't work because other capfs were non-exclusive, as you've mentioned. But I never knew about this property either. This explains everything.

Ideally we document such setups in the Corfu wiki, to help other users who want to migrate. By documenting multiple such setups we show the common pattern. I would appreciate if you contribute there if you figure out a good setup which works for you with Slime/Cider

Of course. I'll play around with this new information, and once everything settles in my config for some time, I'll add this to the wiki.

Thanks again!

minad commented 2 years ago

I guess, when I've tried setting cape capfs as a global value it didn't work because other capfs were non-exclusive, as you've mentioned.

Be careful about this! I doubt that this is the issue in most scenarios! Exclusive has a very specific meaning. It means if a Capf returns a completion table, even if it does not match the input, then it will prevent the other Capfs from running. Non-exclusive means that such a non-matching completion table is then ignored and the next Capf is tried. This means an exclusive Capf can still behave non-exclusively by not returning a completion table at all (and returning nil instead) in situations where it does not trigger. The elisp-completion-at-point-function works in that way for example. It is exclusive but sufficiently non-exclusive ;-) Therefore imo one should just do away with the distinction and treat everything as non-exclusive, but it is usually hard to fix such historical mistakes in Emacs later on.

Generally, be careful about these Cape transformers provided by this package. You should only apply them in specific scenarios, where you are sure that they fix the exact problem. For example Cape comes with transformers/wrappers which are specifically made to fix shell completion, see https://github.com/minad/corfu#completing-with-corfu-in-the-shell-or-eshell.