Skip to content

Use Citre together with lsp mode or eglot

AmaiKinono edited this page Apr 21, 2024 · 3 revisions

lsp-mode is an Emacs package utilizing language servers.

lsp-mode usually does a good job indexing external libraries used by the project, which is usually not tagged in a tags file. At the same time, depends on the implementation, I've heard from lsp-mode users that many language servers are not good at completing or finding definitions of a symbol in the project. So, these two packages could complement each other well.

I (@AmaiKinono) myself is not a lsp-mode user. The below tricks are invented by other users, or confirmed by them to work.

Use Citre xref backend as a fallback

Note: Citre now contains an eglot backend, and tags/global are used as its fallback. For eglot users, the following hack is no longer needed. And for lsp-mode users, you could use citre-xref-backend-to-citre-backend to create a Citre backend from lsp-mode's xref backend, so the following hack is also not needed.

The below advice makes Citre come to rescue when enabled xref backends can't find a definition. So, when you enable the lsp backend, this tries lsp first, then use Citre.

(define-advice xref--create-fetcher (:around (-fn &rest -args) fallback)
  (let ((fetcher (apply -fn -args))
        (citre-fetcher
         (let ((xref-backend-functions '(citre-xref-backend t)))
           (apply -fn -args))))
    (lambda ()
      (or (with-demoted-errors "%s, fallback to citre"
            (funcall fetcher))
          (funcall citre-fetcher)))))

This requires lexical binding to work.

Combine completions from Citre and lsp/eglot

This is a completion-at-point-function that tries lsp first, and if no completions are given, try Citre.

(defun lsp-citre-capf-function ()
  "A capf backend that tries lsp first, then Citre."
  (let ((lsp-result (lsp-completion-at-point)))
    (if (and lsp-result
             (try-completion
              (buffer-substring (nth 0 lsp-result)
                                (nth 1 lsp-result))
              (nth 2 lsp-result)))
        lsp-result
      (citre-completion-at-point))))

(defun enable-lsp-citre-capf-backend ()
  "Enable the lsp + Citre capf backend in current buffer."
  (add-hook 'completion-at-point-functions #'lsp-citre-capf-function nil t))

(add-hook 'citre-mode-hook #'enable-lsp-citre-capf-backend)

Alternatively, if you are using cape, you can merge the completion candidates from lsp and citre into one capf, the following is a example of merging citre completion at point function and eglot completion at point function together:

(defalias #'my-eglot-citre-capf
  (cape-super-capf #'eglot-completion-at-point #'citre-completion-at-point))

(add-hook 'eglot-managed-mode-hook
  (defun my-toggle-citre-eglot-capf ()
    (if (eglot-managed-p)
      (add-to-list 'completion-at-point-functions #'my-eglot-citre-capf))))

If you use company, you can create a company backend from a Citre completion backend, then make a "combined backend" that returns both results from lsp and Citre (Thanks @yyjjl for the tip!).

Here's a macro that translates a Citre completion backend to a company backend:

(defmacro citre-backend-to-company-backend (backend)
  "Create a company backend from Citre completion backend BACKEND.
The result is a company backend called
`company-citre-<backend>' (like `company-citre-tags') and can be
used in `company-backends'."
  (let ((backend-name (intern (concat "company-citre-" (symbol-name backend))))
        (docstring (concat "`company-mode' backend from the `"
                           (symbol-name backend)
                           "' Citre backend.\n"
                           "`citre-mode' needs to be enabled to use this.")))
    `(defun ,backend-name (command &optional arg &rest ignored)
       ,docstring
       (pcase command
         ('interactive (company-begin-backend ',backend-name))
         ('prefix (and (bound-and-true-p citre-mode)
                       (citre-backend-usable-p ',backend)
                       ;; We shouldn't use this as it's defined for getting
                       ;; definitions/references.  But the Citre completion
                       ;; backend design is not fully compliant with company's
                       ;; design so there's no simple "right" solution, and this
                       ;; works for tags/global backends.
                       (or (citre-get-symbol-at-point-for-backend ',backend)
                           'stop)))
         ('meta (citre-get-property 'signature arg))
         ('annotation (citre-get-property 'annotation arg))
         ('candidates (let ((citre-completion-backends '(,backend)))
                        (all-completions arg (nth 2 (citre-completion-at-point)))))))))

Now let's create company-citre-tags and company-citre-global backends, and make a combined backend of them and company-capf, which is used by lsp-mode:

(citre-backend-to-company-backend tags)
(citre-backend-to-company-backend global)
(setq company-backends '((company-capf
                          company-citre-tags
                          company-citre-global
                          :with company-yasnippet
                          :separate)))