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