use-package icon indicating copy to clipboard operation
use-package copied to clipboard

Completion for use-package macro (e.g., within :custom)

Open leinfink opened this issue 1 year ago • 4 comments

I would like to have completion-at-point for use-package. In particular, it would be great if there was a way to let emacs know that inside :custom, it should expect variables, not functions, even though point might look as if it was in a funcall position. If not easily fixable, could someone guide me in the correct direction to work on this?

#277 apparently tried to do something like this, but it didn't seem to lead anywhere.

leinfink avatar Jul 04 '24 15:07 leinfink

I also would like to have improved completion candidates for use-package. I actually switched away from using use-package primarily for this reason. IMHO, calling the underlying functions and macros (e.g. setopt, add-hook) provide a much better sense of discoverability and even (counterintuitively?) save me time and effort because those completions just work. Clearly I rely a lot on completions to help me figure out what I am trying to do :sweat_smile:

I guess a lot of the issue is use-package's extensive use of unquoted lists. But still, even if the lists in :custom, for example, were quoted, you would still get completion candidates for any symbol, not just variables. Callables like setopt and keymap-set offer out-of-the-box completion appropriate to the context, and they don't require everything to be contained in extraneous lists like :custom and :bind.

astratagem avatar Jul 17 '24 00:07 astratagem

Since when, I think Emacs assumes that the :custom symbols are functions, not variables, and makes code jumps. The same issue is happening in leaf and I don't know how to solve this problem. As a work-around, for example, when there is a use-package like this,

(use-package comint
  :custom
  (comint-buffer-maximum-size 20000 “Increase comint buffer size.”)
  (comint-prompt-read-only t “Make the prompt read only.”))
(use-package comint
  :custom
  (a comint-buffer-maximum-size 20000 “Increase comint buffer size.”))
  (comint-prompt-read-only t “Make the prompt read only.”)))

Change it this way, and for comint-buffer-maximum-size, M-. would allow me to jump properly, but it's too bad work-around.

conao3 avatar Jul 17 '24 03:07 conao3

The default capf used in elisp-mode, elisp-completion-at-point, filters completion candidates based on context (variables, functions, ...). The problem is that this filter cannot recognize variables in :custom of use-package.

This is not a complete solution, but I am working around the issue by setting (cape-capf-inside-code #'cape-elisp-symbol) to local variable completion-at-point-function in elisp mode. I distinguish whether a completion candidate is a function or a variable using icon information provided by nerd-icons-corfu. If you are using company, you can achieve the same with company-box.

Here is a part of my init.el and screenshot:

(defun my/elisp-mode-init ()
  "Set completion function to cape"
  (setq-local completion-at-point-functions
              (list (cape-capf-inside-code #'cape-elisp-symbol))))
(add-hook 'emacs-lisp-mode-hook #'my/elisp-mode-init)

image

yonta avatar Aug 03 '24 08:08 yonta

My workaround is the following:

(defun my/elisp-custom-keyword-lax-capf ()
  "Provide lax completion when in s-expr with preceding :custom keyword."
  (when (and (derived-mode-p 'emacs-lisp-mode)
             (my/elisp-custom-keyword-lax-capf--pred))
    ;; get elisp-capf result
    (when-let ((result (elisp-completion-at-point)))
      ;; capf new
      (append (take 3 result)
              (list :annotation-function
                    (lambda (cand)
                      (let ((sym (intern-soft cand)))
                        (cond
                         ((and sym (boundp sym)) " <var>")
                         ((and sym (fboundp sym)) " <func>")
                         ((keywordp sym) " <key>")
                         (t "")))))))))

(defun my/elisp-custom-keyword-lax-capf--pred ()
  "Predicate for `my/elisp-custom-keyword-lax-capf'.

Checks if the point is under `use-package' or `leaf',
and that the last keyword was :custom."
  (when-let*
      ((limit
        (save-excursion
          (condition-case nil
              ;; go backwards-up till find use-package or leaf
              (progn
                (while (not (looking-at-p "(\\(use-package\\|leaf\\)\\b"))
                  (backward-up-list))
                (point))
            ;; no matches
            (error nil)))))
    ;; search backwards, find last keyword, if ":custom" ret t
    (save-excursion
      (when (re-search-backward " \\(:\\w+\\)" limit t)
        (string= (match-string 1) ":custom")))))

(add-hook 'emacs-lisp-mode-hook
          (lambda ()
            (add-hook 'completion-at-point-functions
                      #'my/elisp-custom-keyword-lax-capf nil t)))

I create a new capf with looser category restrictions (includes vars, funcs, etc) that activates only if:

  • current mode is emacs-lisp-mode
  • pseudocode:
    • let start : save starting point.
    • when-let limit : searching backwards-up the s-exprs until find symbol use-package, save that point.
      • from point start, up until point limit, search backwards until find keyword. if keyword is :custom, follow through with capf.

It's important to note that the #'my/elisp-custom-keyword-lax-capf in the 'completion-at-point-functions hook needs to come before #'elisp-completion-at-point, so that the new capf has the chance to trigger before the native elisp one does.

lispcat avatar Aug 15 '25 23:08 lispcat