minad / cape

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

Create `cape-yasnippet` backend for yasnippet failed #37

Closed stardiviner closed 2 years ago

stardiviner commented 2 years ago

I tried to following guide section "Company adapter" on README.

README guide section example code

(defvar emojis
  '((":-D" . "😀")
    (";-)" . "😉")
    (":-/" . "😕")
    (":-(" . "🙁")
    (":-*" . "😙")))

(defun emoji-backend (action &optional arg &rest _)
  (pcase action
    ('prefix (and (memq (char-before) '(?: ?\;))
                  (cons (string (char-before)) t)))
    ('candidates (all-completions arg emojis))
    ('annotation (concat " " (cdr (assoc arg emojis))))
    ('post-completion
     (let ((str (buffer-substring (- (point) 3) (point))))
       (delete-region (- (point) 3) (point))
       (insert (cdr (assoc str emojis)))))))

;; Register emoji backend with `completion-at-point'
(setq completion-at-point-functions
      (list (cape-company-to-capf #'emoji-backend)))

;; Register emoji backend with Company.
(setq-local company-backends '(emoji-backend))

I tested this example code, and it works fine so that I can confirm cape is fine.

company-yasnippet source code

I copied most code from company-yasnippet original source code as reference:

(defun company-yasnippet (command &optional arg &rest ignore)
  "`company-mode' backend for `yasnippet'.

This backend should be used with care, because as long as there are
snippets defined for the current major mode, this backend will always
shadow backends that come after it.  Recommended usages:

* In a buffer-local value of `company-backends', grouped with a backend or
  several that provide actual text completions.

  (add-hook \\='js-mode-hook
            (lambda ()
              (set (make-local-variable \\='company-backends)
                   \\='((company-dabbrev-code company-yasnippet)))))

* After keyword `:with', grouped with other backends.

  (push \\='(company-semantic :with company-yasnippet) company-backends)

* Not in `company-backends', just bound to a key.

  (global-set-key (kbd \"C-c y\") \\='company-yasnippet)
"
  (interactive (list 'interactive))
  (cl-case command
    (interactive (company-begin-backend 'company-yasnippet))
    (prefix
     ;; Should probably use `yas--current-key', but that's bound to be slower.
     ;; How many trigger keys start with non-symbol characters anyway?
     (and (bound-and-true-p yas-minor-mode)
          (company-grab-symbol)))
    (annotation
     (funcall company-yasnippet-annotation-fn
              (get-text-property 0 'yas-annotation arg)))
    (candidates (company-yasnippet--candidates arg))
    (doc-buffer (company-yasnippet--doc arg))
    (no-cache t)
    (kind 'snippet)
    (post-completion
     (let ((template (get-text-property 0 'yas-template arg))
           (prefix-offset (get-text-property 0 'yas-prefix-offset arg)))
       (yas-expand-snippet (yas--template-content template)
                           (- (point) (length arg) prefix-offset)
                           (point)
                           (yas--template-expand-env template))))))

(defun company-yasnippet--candidates (prefix)
  ;; Process the prefixes in reverse: unlike Yasnippet, we look for prefix
  ;; matches, so the longest prefix with any matches should be the most useful.
  (cl-loop with tables = (yas--get-snippet-tables)
           for key-prefix in (company-yasnippet--key-prefixes)
           ;; Only consider keys at least as long as the symbol at point.
           when (>= (length key-prefix) (length prefix))
           thereis (company-yasnippet--completions-for-prefix prefix
                                                              key-prefix
                                                              tables)))

(defvar company-yasnippet-annotation-fn
  (lambda (name)
    (concat
     (unless company-tooltip-align-annotations " -> ")
     name))
  "Function to format completion annotation.
It has to accept one argument: the snippet's name.")

(defun company-grab-symbol ()
  "If point is at the end of a symbol, return it.
Otherwise, if point is not inside a symbol, return an empty string."
  (if (looking-at "\\_>")
      (buffer-substring (point) (save-excursion (skip-syntax-backward "w_")
                                                (point)))
    (unless (and (char-after) (memq (char-syntax (char-after)) '(?w ?_)))
      "")))

first try by extracting code from company-yasnippet

;; reference `company-yasnippet'.
(defun cape-yasnippet (action &optional arg &rest _)
  "Complete snippet at point with yasnippet."
  (pcase action
    ('prefix
     ;; Should probably use `yas--current-key', but that's bound to be slower.
     ;; How many trigger keys start with non-symbol characters anyway?
     (and (bound-and-true-p yas-minor-mode)
          ;; `company-grab-symbol'
          (if (looking-at "\\_>")
              (buffer-substring (point) (save-excursion (skip-syntax-backward "w_") (point)))
            (unless (and (char-after) (memq (char-syntax (char-after)) '(?w ?_))) ""))))
    ('candidates
     ;; `company-yasnippet--candidates'
     (let ((prefix arg))
       (cl-loop with tables = (yas--get-snippet-tables)
                for key-prefix in (company-yasnippet--key-prefixes)
                ;; Only consider keys at least as long as the symbol at point.
                when (>= (length key-prefix) (length prefix))
                thereis (company-yasnippet--completions-for-prefix prefix
                                                                   key-prefix
                                                                   tables))))
    ('annotation
     (funcall '(lambda (name) (concat " -> " name)) (get-text-property 0 'yas-annotation arg)))
    ('post-completion
     (let ((template (get-text-property 0 'yas-template arg))
           (prefix-offset (get-text-property 0 'yas-prefix-offset arg)))
       (yas-expand-snippet (yas--template-content template)
                           (- (point) (length arg) prefix-offset)
                           (point)
                           (yas--template-expand-env template))))))

(setq-local completion-at-point-functions '(cape-yasnippet cape-symbol))

This report error:

Debugger entered--Lisp error: (wrong-number-of-arguments (lambda (action &optional arg &rest _) "Complete snippet at point with yasnippet." (cond ((eq action 'prefix) (let nil (and (and (boundp ...) yas-minor-mode) (if (looking-at "\\_>") (buffer-substring ... ...) (if ... nil ""))))) ((eq action 'candidates) (let nil (let ((prefix arg)) (let* (... ... ... ... --cl-var--) (while ... ...) --cl-var--)))) ((eq action 'annotation) (let nil (funcall '(lambda ... ...) (get-text-property 0 'yas-annotation arg)))) ((eq action 'post-completion) (let nil (let ((template ...) (prefix-offset ...)) (yas-expand-snippet (progn ... ...) (- ... ... prefix-offset) (point) (progn ... ...))))))) 0)
  cape-yasnippet()
  corfu--capf-wrapper(cape-yasnippet)
  run-hook-wrapped(corfu--capf-wrapper cape-yasnippet)
  corfu--auto-complete((#<buffer init-emacs-text-complete.el> 27509 18712))
  apply(corfu--auto-complete (#<buffer init-emacs-text-complete.el> 27509 18712))
  timer-event-handler([t 25202 15611 594805 nil corfu--auto-complete ((#<buffer init-emacs-text-complete.el> 27509 18712)) nil 0 nil])

second try with directly invoke from company-yasnippet internal API

(defun cape-asnippet (action &optional arg &rest _)
  (cl-case action
    (prefix
     ;; Should probably use `yas--current-key', but that's bound to be slower.
     ;; How many trigger keys start with non-symbol characters anyway?
     (and (bound-and-true-p yas-minor-mode)
          (company-grab-symbol)))
    (annotation
     (funcall company-yasnippet-annotation-fn
              (get-text-property 0 'yas-annotation arg)))
    (candidates (company-yasnippet--candidates arg))
    (doc-buffer (company-yasnippet--doc arg))
    (no-cache t)
    (kind 'snippet)
    (post-completion
     (let ((template (get-text-property 0 'yas-template arg))
           (prefix-offset (get-text-property 0 'yas-prefix-offset arg)))
       (yas-expand-snippet (yas--template-content template)
                           (- (point) (length arg) prefix-offset)
                           (point)
                           (yas--template-expand-env template))))))

(setq-local completion-at-point-functions '(cape-yasnippet cape-symbol))

This still got same error:

Debugger entered--Lisp error: (wrong-number-of-arguments (lambda (action &optional arg &rest _) "Complete snippet at point with yasnippet." (cond ((eq action 'prefix) (let nil (and (and (boundp ...) yas-minor-mode) (if (looking-at "\\_>") (buffer-substring ... ...) (if ... nil ""))))) ((eq action 'candidates) (let nil (let ((prefix arg)) (let* (... ... ... ... --cl-var--) (while ... ...) --cl-var--)))) ((eq action 'annotation) (let nil (funcall '(lambda ... ...) (get-text-property 0 'yas-annotation arg)))) ((eq action 'post-completion) (let nil (let ((template ...) (prefix-offset ...)) (yas-expand-snippet (progn ... ...) (- ... ... prefix-offset) (point) (progn ... ...))))))) 0)
  cape-yasnippet()
  corfu--capf-wrapper(cape-yasnippet)
  run-hook-wrapped(corfu--capf-wrapper cape-yasnippet)
  corfu--auto-complete((#<buffer init-emacs-text-complete.el> 27812 18713))
  apply(corfu--auto-complete (#<buffer init-emacs-text-complete.el> 27812 18713))
  timer-event-handler([t 25202 15686 844144 nil corfu--auto-complete ((#<buffer init-emacs-text-complete.el> 27812 18713)) nil 0 nil])

my question

I confirmed the function cape-yasnippet arguments (action &optional arg &rest _) it is same with emoji-backend. I don't understand why wrong-number-of-arguments.

stardiviner commented 2 years ago

Oh, I mis-read the emoji-backend example code, it use cape-company-to-capf adapter.

minad commented 2 years ago

Hmm, this question goes a bit too far for me to help. I suggest you either use cape-company-to-capf and wrap company-yasnippet. Really, there is nothing wrong with that! We just reuse existing Company backends in normal Capf completion (Corfu, default completion). The Company frontend will not be used, but the mere presence of the Company frontend code will not hurt your Emacs installation. Alternatively create a package capf-yasnippet or add a Capf directly to yasnippet. You can take the Cape Capfs for inspiration but I recommend to not depend on cape. See also #17 where cape-yasnippet was proposed. Furthermore maybe you are interested in my Tempel package, which I use instead of Yasnippet?

stardiviner commented 2 years ago

I have writed a simple cape-yasnippet backend which can display snippet now. But need to add some extra features like:

I will reference code in cape-company-to-capf etc functions about how it hands this properties.

Here is the first version workable code:

(defun cape--yasnippet-snippets (prefix)
  "Return all snippets from yasnippet matching PREFIX"
  (require 'yasnippet)
  ;; `company-yasnippet--candidates'
  (cl-loop with tables = (yas--get-snippet-tables)
           for key-prefix in (company-yasnippet--key-prefixes)
           ;; Only consider keys at least as long as the symbol at point.
           when (>= (length key-prefix) (length prefix))
           thereis (company-yasnippet--completions-for-prefix prefix
                                                              key-prefix
                                                              tables)))

(defvar cape--yasnippet-properties
  (list :annotation-function (lambda (_) " snippet")
        :company-kind (lambda (_) 'snippet)
        :exclusive 'no)
  "Completion extra properties for `cape-yasnippet'.")

(defun cape-yasnippet (&optional interactive)
  "Complete snippet at point with yasnippet.
If INTERACTIVE is nil the function acts like a Capf."
  (interactive (list t))
  (if interactive
      (cape--interactive #'cape-yasnippet)
    (let ((bounds (cape--bounds 'word)))
      `(,(car bounds) ,(cdr bounds)
        ,(cape--table-with-properties
          (cape--cached-table (car bounds) (cdr bounds) #'cape--yasnippet-snippets 'substring)
          :category 'cape-yasnippet)
        ,@cape--yasnippet-properties))))

(setq-local completion-at-point-functions '(cape-yasnippet cape-symbol))
stardiviner commented 2 years ago

Thanks for your suggestion. I read them recently and try to study out a solution.

I suggest you either use cape-company-to-capf and wrap company-yasnippet. Really, there is nothing wrong with that! We just reuse existing Company backends in normal Capf completion (Corfu, default completion).

I already tried this method, but it's difficult to customize the completion-at-point-functions then. For example:

(setq-local completion-at-point-functions
            (list
             (mapcar #'cape-company-to-capf (list #'company-yasnippet #'company-elisp))
             #'cape-file #'cape-keyword #'cape-abbrev #'cape-dabbrev #'cape-ispell
             ))

or like this:

(setq-local completion-at-point-functions
            (list
             (cape-company-to-capf
              (apply-partially #'company--multi-backend-adapter
                               '(company-yasnippet company-elisp)))
             #'cape-file #'cape-keyword #'cape-abbrev #'cape-dabbrev #'cape-ispell
             ))

They can't archive what I want.

See also https://github.com/minad/cape/issues/17 where cape-yasnippet was proposed. Furthermore maybe you are interested in my Tempel package, which I use instead of Yasnippet?

I indeed checked out temple package, but porting yasnippet snippets is a big work. So I try to stick on yasnippet for now.

minad commented 2 years ago

They can't archive what I want.

Why not?

stardiviner commented 2 years ago

They can't archive what I want.

Why not?

My upper first completion-at-point-functions setting has problem. The second setting works fine. I will take a little time to figure out the :exit-function for yasnippet expanding. Once figured out, I can lose Emacs config weight of company dependency. If not, I will take the cape-company-to-capf solution.

Might some user want to do same thing, just keep some process here.

toniz4 commented 1 year ago

They can't archive what I want.

Why not?

My upper first completion-at-point-functions setting has problem. The second setting works fine. I will take a little time to figure out the :exit-function for yasnippet expanding. Once figured out, I can lose Emacs config weight of company dependency. If not, I will take the cape-company-to-capf solution.

Might some user want to do same thing, just keep some process here.

I managed to come up with something, copyed half your code and half company's code (the wonders of free software). This only depends on cape and yasnippet

(defun cape-yasnippet--key-prefixes ()
  ;; Copied from `company-yasnippet--key-prefixes'.
  (defvar yas-key-syntaxes)
  (save-excursion
    (let ((original (point))
          (methods yas-key-syntaxes)
          prefixes
          method)
      (while methods
        (unless (eq method (car methods))
          (goto-char original))
        (setq method (car methods))
        (cond ((stringp method)
               (skip-syntax-backward method)
               (setq methods (cdr methods)))
              ((functionp method)
               (unless (eq (funcall method original)
                           'again)
                 (setq methods (cdr methods))))
              (t
               (setq methods (cdr methods))
               (yas--warning "Invalid element `%s' in `yas-key-syntaxes'" method)))
        (let ((prefix (buffer-substring-no-properties (point) original)))
          (unless (equal prefix (car prefixes))
            (push prefix prefixes))))
      prefixes)))

(defun cape-yasnippet--completions-for-prefix (prefix key-prefix tables)
  ;; Copied from `cape-yasnippet--completions-for-prefix'.
  (cl-mapcan
   (lambda (table)
     (let ((keyhash (yas--table-hash table))
           (requirement (yas--require-template-specific-condition-p))
           res)
       (when keyhash
         (maphash
          (lambda (key value)
            (when (and (stringp key)
                       (string-prefix-p key-prefix key))
              (maphash
               (lambda (name template)
                 (when (yas--template-can-expand-p
                        (yas--template-condition template) requirement)
                   (push
                    (propertize key
                                'yas-annotation name
                                'yas-template template
                                'yas-prefix-offset (- (length key-prefix)
                                                      (length prefix)))
                    res)))
               value)))
          keyhash))
       res))
   tables))

(defun cape--yasnippet-snippets (prefix)
  "Return all snippets from yasnippet matching PREFIX"
  (require 'yasnippet)
  (cl-loop with tables = (yas--get-snippet-tables)
           for key-prefix in (cape-yasnippet--key-prefixes)
           ;; Only consider keys at least as long as the symbol at point.
           when (>= (length key-prefix) (length prefix))
           thereis (cape-yasnippet--completions-for-prefix prefix
                                                              key-prefix
                                                              tables)))
(defvar cape--yasnippet-properties
  (list :annotation-function (lambda (_) " snippet")
        :company-kind (lambda (_) 'snippet)
        :exit-function (lambda (_ status) 
             (when (eq status 'finished)
               (yas-expand)))
        :exclusive 'no)
  "Completion extra properties for `cape-yasnippet'.")

(defun cape-yasnippet (&optional interactive)
  "Complete snippet at point with yasnippet.
If INTERACTIVE is nil the function acts like a Capf."
  (interactive (list t))
  (if interactive
      (cape--interactive #'cape-yasnippet)
    (let ((bounds (cape--bounds 'word)))
      `(,(car bounds) ,(cdr bounds)
        ,(cape--table-with-properties
          (cape--cached-table (car bounds) (cdr bounds) #'cape--yasnippet-snippets 'substring)
          :category 'cape-yasnippet)
        ,@cape--yasnippet-properties))))

Solved your issue with the :exit-function with:

(lambda (_ status) 
  (when (eq status 'finished)
  (yas-expand)))

Basicaly if I accept the completion on corfu it expands, don't know if it's the right way to do it, I tested it for a solid 2 minutes and have any idea on what I'm doing.

ymfsing commented 1 year ago

this maybe help

https://github.com/elken/cape-yasnippet

stardiviner commented 1 year ago

Thanks, already marked.