Use citar to define a completion‐at‐point function for bibtex files - emacs-citar/citar GitHub Wiki

If you manage your .bib files using Emacs, it can be convenient to have a completion-at-point function when writing the values for certain fields. Here's a small library that defined such a function. It can optionally make use of the citar hash table, thus avoiding having to parse each file in your bibtex-files. (This is also available here, warts and all.)

;;; bib-capf.el --- Completion at point for BibTeX fields with optional citar integration -*- lexical-binding: t; -*-

;; Keywords: bibtex, convenience
;; Version: 0.1
;; Package-Requires: ((emacs "27.1"))



;;; Commentary:

;; This package provides a `completion-at-point-function` (CAPF) for
;; completing BibTeX field values (such as author names, journal
;; titles, publishers, etc.) using entries from the current buffer.
;; Optionally, it can use all files in `bibtex-files' or,
;; alternatively, the hash table generated by the citar package:
;;
;; https://github.com/emacs-citar/citar  


;;; Code:

(require 'bibtex)
(require 'seq)

(defgroup bib-capf nil
  "Completion at point for BibTeX fields using citar."
  :group 'bibtex
  :prefix "bib-capf-")

(defcustom bib-capf-source 'local
  "Specify the source for BibTeX field values.
If set to 'citar', the citar package will be required.

Tested with citar version 1.4.0."
  :type '(choice (const :tag "Buffer only" 'local)
                 (const :tag "Citar" 'citar)
                 (const :tag "Bibtex Files" 'bibtex-files))
  :group 'bib-capf
  :set (lambda (symbol value)
         (set symbol value)
         (when (eq value 'citar)
           (require 'citar))))

(defcustom bib-capf-fields
  '("journaltitle" "author" "editor" "publisher" "location" "address" "booktitle")
  "List of BibTeX fields (canonical names) for which to provide completion."
  :type '(repeat string))

(defcustom bib-capf-multi-val-fields '("author" "editor")
  "List of fields whose values are to be parsed at ‘and’."
  :type '(repeat string))

(defvar bib-capf--field-aliases
  '(("journal" . "journaltitle")
    ("address" . "location"))
  "Mapping of field aliases to canonical field names.")

(defun bib-capf--normalize-field (field)
  "Return canonical field name for FIELD, resolving aliases."
  (or (cdr (assoc field bib-capf--field-aliases))
      field))

(defun bib-capf--parse-field ()
  "Parse structure returned by `bibtex-find-text-internal' into a plist.
Result includes field name and bounds for the field value.

For our purposes, bounds are always in the enclosing braces."
  (let* ((info (bibtex-find-text-internal))
         (field (nth 0 info))
         (beg (1+ (nth 1 info)))
         (end (1- (nth 2 info))))
    (list :field field :beg beg :end end)))

(defun bib-capf--bounds-of-field-value (&optional include-field-name)
  "Return (START . END) bounds for the BibTeX field value around point.
If INCLUDE-FIELD-NAME is non-nil, return an alist containing the field name too."
  (save-excursion
    (let* ((bounds (bib-capf--parse-field))
           (multi-name-fields bib-capf-multi-val-fields))
      (when bounds
        (let* ((field-name (plist-get bounds :field))
               (initial-point (point))
               (content-start (plist-get bounds :beg))
               (content-end (plist-get bounds :end)))
          (when (and content-start content-end (>= content-end content-start))
            (let* ((start-end
                    (if (member (downcase (string-trim field-name)) multi-name-fields)
                        ;; Bounds for multi-name fields at ‘and’
                        (let ((name-start)
                              (name-end))
                          (goto-char initial-point)
                          (setq name-start
                                (or (and (re-search-backward "\\s-+and\\s-+" content-start t)
                                         (goto-char (match-end 0))
                                         (point))
                                    content-start))
                          (setq name-end
                                (or (and (re-search-forward "\\s-+and\\s-+" content-end t)
                                         (goto-char (match-beginning 0))
                                         (point))
                                    content-end))
                          (cons name-start name-end))
                      ;; otherwise, just use content-start and content-end
                      (cons content-start content-end))))
              (if include-field-name
                  `((field . ,(downcase (string-trim field-name)))
                    (bounds . ,start-end))
                start-end))))))))


(defun bib-capf--citar-get-field-values (field)
  "Return a list of unique non-empty FIELD values from the citar cache."
  (let ((entries (citar-cache--entries (citar--bibliographies))))
    (if (not entries)
        (progn
          (message "bib-capf: BibTeX cache is empty or unavailable.")
          '())
      (let ((values '()))
        (maphash
         (lambda (_key entry)
           (let ((field-value (cdr (assoc field entry))))
             (cond
              ((and (listp field-value) field-value)
               (dolist (v field-value)
                 (when (and v (not (string-empty-p v)))
                   (push (string-trim v) values))))
              ((and (stringp field-value) (not (string-empty-p field-value)))
               (push (string-trim field-value) values)))))
         entries)
        (delete-dups values)))))


(defun bib-capf--clean-field-value (val)
  "Clean a BibTeX field VAL by removing surrounding braces or quotes."
  (when val
    (string-trim
     (replace-regexp-in-string
      "\\`[\"{]\\(.*?\\)[\"}]\\'" "\\1"
      val))))

(defun bib-capf--bibtex-get-buffer-field-values (field)
  (let (raw-vals)
    (bibtex-map-entries
     (lambda (_key beg end)
       (save-excursion
         (goto-char beg)
         (let* ((entry (bibtex-parse-entry))
                (val (bib-capf--clean-field-value
                      (cdr (assoc-string field entry t)))))
           (when val
             (push val raw-vals))))))
    raw-vals))

(defun bib-capf--bibtex-get-field-values (field &optional global)
  (if (not global)
      (bib-capf--bibtex-get-buffer-field-values field)
    (let (all-vals)
      (dolist (file bibtex-files)
        (if (file-exists-p file)
          (with-temp-buffer
            (insert-file-contents file)
            (bibtex-mode)
            (setq all-vals (append all-vals (bib-capf--bibtex-get-buffer-field-values field))))
          (message "Skipping: %s cannot be found" file)))
      all-vals)))

(defun bib-capf--get-field-values (field)
  "Return a list of unique non-empty FIELD values based on `bib-capf-source`."
  (cond
   ;; Use Citar cache if `bib-capf-source` is 'citar
   ((eq bib-capf-source 'citar)
    (bib-capf--citar-get-field-values field))
   ;; Use BibTeX files if `bib-capf-source` is 'bibtex-files
   ((eq bib-capf-source 'bibtex-files)
    (bib-capf--bibtex-get-field-values field t))
   ;; Use local buffer if `bib-capf-source` is 'local
   ((eq bib-capf-source 'local)
    (bib-capf--bibtex-get-field-values field))
   ;; In case of invalid value, return an empty list
   (t
    (error "Invalid `bib-capf-source` value"))))

(defun bib-capf--collect-field-values (field)
  "Collect all unique, non-trivial values for FIELD.
Handles splitting multi-name fields like `author' and `editor'."
  (let* ((canonical (bib-capf--normalize-field field))
         (multi-name-fields '("author" "editor"))
         (values (let ((raw-values (bib-capf--get-field-values canonical)))
                   (if (member canonical multi-name-fields)
                       (apply #'append
                              (mapcar (lambda (val)
                                        (mapcar #'string-trim
                                                (split-string val "\\s-+and\\s-+" t)))
                                      raw-values))
                     raw-values))))
    (seq-remove #'string-blank-p
                (sort (delete-dups values) #'string<))))

(defun bib-capf--annotation-str (_)
  "Convert the value of SOURCE into a corresponding string."
  (let ((source bib-capf-source))
    (cond
     ((eq source 'citar) " citar")
     ((eq source 'local) " buffer")
     ((eq source 'bibtex-files) " bibtex-files")
     nil)))

(defun bib-capf--completion-at-point ()
  "Provide completion at point for BibTeX fields using citar cache."
  (when (derived-mode-p 'bibtex-mode)
    (let* ((data (bib-capf--bounds-of-field-value t))
           (field (alist-get 'field data))
           (bounds (alist-get 'bounds data)))
      (when (member field bib-capf-fields)
        (let ((candidates (bib-capf--collect-field-values field)))
          (when (and bounds candidates)
            (list (car bounds) (cdr bounds) candidates
                  :exclusive 'no
                  :annotation-function #'bib-capf--annotation-str)))))))

;;;###autoload
(defun bib-capf-setup ()
  "Enable BibTeX field value completion at point using citar."
  (add-hook 'completion-at-point-functions
            #'bib-capf--completion-at-point
            0 t))

(provide 'bib-capf)

;;; bib-capf.el ends here