;;; org-ref-latex.el --- org-ref functionality for LaTeX files -*- lexical-binding: t; -*- ;; Copyright (C) 2015-2024 John Kitchin ;; Author: John Kitchin ;; Keywords: languages ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: Make cites in LaTeX documents clickable, and with tooltips. ;; We use font-lock to add some functionality to the keys. ;;; Code: (require 'org-ref-citation-links) (require 'bibtex-completion) (require 'tex-mode) (defvar org-ref-cite-types) (defcustom orl-cite-keymap (let ((map (copy-keymap latex-mode-map))) (define-key map (kbd "S-") #'orl-cite-shift-left) (define-key map (kbd "S-") #'orl-cite-shift-right) (define-key map (kbd "S-") #'orl-sort-cite-keys) (define-key map (kbd "S-") (lambda () (interactive) (orl-sort-cite-keys t))) (define-key map (kbd "") #'org-ref-latex-insert-citation) (define-key map [mouse-1] (lambda () (interactive) (let ((bibtex-completion-bibliography (org-ref-latex-get-bibliography))) (bibtex-completion-show-entry (list (org-ref-latex-key-at-point))) (bibtex-beginning-of-entry)))) map) "Key map for cite keys." :group 'org-ref-latex :type '(restricted-sexp :match-alternatives ((lambda (x) (or (keymapp x) ; Can be a keymap var. (and (symbolp x) (boundp x) (keymapp (symbol-value x)))))))) (defvar org-ref-latex-cite-re (concat "\\\\\\(?1:" (mapconcat (lambda (x) (replace-regexp-in-string "\\*" "\\\\*" x)) (mapcar 'car org-ref-cite-types) "\\|") "\\)" "\\(?2:\\[[^]]*\\]\\)?" ; optional [] "\\(?3:\\[[^]]*\\]\\)?" ; optional [] "{\\(?4:[^}]*\\)}") ; group 4 contains the keys "Regexp for LaTeX citations. \\citetype[opti{o}nal][optiona{l}]{some,keys}. The clickable part are the keys.") (defun org-ref-latex--scan-buffer-for-bibliography () "Scan current buffer for bibliography commands. Returns a list of bibliography files found via \\bibliography{} and \\addbibresource{} commands. This is a helper function for `org-ref-latex-get-bibliography'." (save-excursion (let ((bibliography '())) ;; Look for \bibliography{file1,file2,...} (BibTeX format) (goto-char (point-min)) (while (re-search-forward "\\\\bibliography{\\(?1:.*\\)}" nil t) (setq bibliography (append bibliography (mapcar (lambda (f) (concat f ".bib")) (split-string (match-string-no-properties 1) ","))))) ;; Look for \addbibresource{file.bib} (BibLaTeX format) (goto-char (point-min)) (while (re-search-forward "\\\\addbibresource{\\(?1:.*\\)}" nil t) (setq bibliography (append bibliography (list (match-string-no-properties 1))))) bibliography))) (defun org-ref-latex-get-bibliography () "Find bibliographies in the tex file. First searches the current buffer for \\bibliography{} or \\addbibresource{} commands. If no bibliography is found and the `TeX-master' variable is set to a filename (indicating this is an included file in a multi-file project), searches the master file for bibliography commands. This enables bibliography discovery in multi-file LaTeX projects where chapter/section files are included into a main file that contains the bibliography declaration. Returns a list of bibliography file paths." (let ((bibliography (org-ref-latex--scan-buffer-for-bibliography))) ;; If no bibliography found in current buffer, check TeX-master (when (and (null bibliography) (boundp 'TeX-master) (stringp TeX-master)) ;; TeX-master is a filename (relative or absolute) ;; Build the full path to the master file (let* ((master-file (expand-file-name ;; Add .tex extension if not present (if (string-match-p "\\.tex\\'" TeX-master) TeX-master (concat TeX-master ".tex")) ;; Resolve relative to current buffer's directory (file-name-directory (buffer-file-name)))) (master-buffer (when (file-exists-p master-file) (find-file-noselect master-file)))) (when master-buffer (with-current-buffer master-buffer (setq bibliography (org-ref-latex--scan-buffer-for-bibliography)))))) bibliography)) (defun org-ref-next-latex-cite (&optional limit) "Font-lock function to make cites in LaTeX documents clickable." (while (re-search-forward org-ref-latex-cite-re limit t) (setq font-lock-extra-managed-props (delq 'help-echo font-lock-extra-managed-props)) ;; save data on cite. This is useful later for modifying cites, clicking, etc. (add-text-properties (match-beginning 0) (match-end 0) `(org-ref-latex-cite ( :cite-begin ,(match-beginning 0) :cite-end ,(match-end 0) :cite-key-begin ,(+ 1 (match-beginning 4)) :cite-key-end ,(- (match-end 4) 1) :cite-type ,(match-string-no-properties 1) :optional-1 ,(match-string-no-properties 2) :optional-2 ,(match-string-no-properties 3) :keys ,(mapcar #'string-trim (split-string (match-string-no-properties 4) ","))))) (goto-char (match-beginning 0)) (let ((end (match-end 0))) (cl-loop for key in (mapcar #'string-trim (split-string (match-string-no-properties 4) ",")) unless (string-empty-p key) do (save-match-data (search-forward key) (add-text-properties (match-beginning 0) (match-end 0) `( org-ref-cite-key ,key mouse-face highlight local-map ,orl-cite-keymap help-echo ,(let* ((bibtex-completion-bibliography (org-ref-latex-get-bibliography))) (condition-case nil (bibtex-completion-apa-format-reference key) (error (display-warning :warning (format "Key %s missing." key))))))))) (goto-char end)))) (defun org-ref-latex-cite-on () "Add the font-lock on for citations." (font-lock-add-keywords 'latex-mode '((org-ref-next-latex-cite 0 font-lock-constant-face)))) (add-hook 'LaTeX-mode-hook 'org-ref-latex-cite-on) (add-hook 'LaTeX-mode-hook 'org-ref-latex-cite-on) ;; * Insert cite link (defun org-ref-latex-on-citation-p () "Return non-nil if on a citation." (get-text-property (point) 'org-ref-latex-cite)) (defun org-ref-latex-key-at-point () "Return key at point if there is one." (interactive) (get-text-property (point) 'org-ref-cite-key)) (defun orl-insert-key (key) (cond ;; on a key ((get-text-property (point) 'org-ref-cite-key) (forward-sexp) (insert (format ",%s" key))) ;; on the leading \ ((and (get-text-property (point) 'org-ref-latex-cite) (looking-at "\\\\")) (search-forward "{") (insert (format "%s," key))) ;; on { ((and (get-text-property (point) 'org-ref-latex-cite) (looking-at "{")) (forward-char) (insert (format "%s," key))) ;; on } ((and (get-text-property (point) 'org-ref-latex-cite) (looking-at "}")) (insert (format ",%s" key))) ;; on citetype ((and (get-text-property (point) 'org-ref-latex-cite) (get-text-property (point) 'org-ref-latex-cite-type)) (search-forward "{") (insert (format "%s," key))) ;; everything else (t (insert (format "\\cite{%s}" key))))) (defun org-ref-latex-insert-citation () "Insert a citation key with completion. Should DWIM inserting into existing or new citations." (interactive) ;; This initializes bibtex if the variable is not defined. (unless bibtex-completion-display-formats-internal (bibtex-completion-init)) (let* ((bibtex-completion-bibliography (org-ref-latex-get-bibliography)) (candidates (bibtex-completion-candidates))) (ivy-read "org-ref-ivy BibTeX entries: " candidates :preselect (ivy-thing-at-point) :action '(1 ("o" (lambda (candidate) (orl-insert-key (cdr (assoc "=key=" (cdr candidate))))) "insert"))))) (defun orl-swap-citation (direction) (let* ((key (org-ref-latex-key-at-point)) (data (get-text-property (point) 'org-ref-latex-cite)) (keys (plist-get data :keys)) (i (seq-position keys key #'string=)) cite-string) (if (> direction 0) (org-ref-swap-list-elements i (min (+ i 1) (- (length keys) 1)) keys) (org-ref-swap-list-elements i (max (- i 1) 0) keys)) (setq data (plist-put data :keys keys)) ;; now render it (setq cite-string (concat (format "\\%s" (plist-get data :cite-type)) (or (plist-get data :optional-1) "") (or (plist-get data :optional-2) "") (format "{%s}" (string-join (plist-get data :keys) ",")))) (cl--set-buffer-substring (plist-get data :cite-begin) (plist-get data :cite-end) cite-string) (search-forward key) (goto-char (match-beginning 0)))) (defun orl-cite-shift-left () "Shift cite key at point to the left." (interactive) (orl-swap-citation -1)) (defun orl-cite-shift-right () "Shift cite-key at point to the right." (interactive) (orl-swap-citation +1)) (defun orl-sort-cite-keys (&optional descending) "Sort keys in ascending order by year. With prefix arg DESCENDING, sort in descending order." (interactive "P") (let* ((data (get-text-property (point) 'org-ref-latex-cite)) (keys (plist-get data :keys)) cite-string) (setq data (plist-put data :keys (cl-loop for entry in (cl-sort (cl-loop for key in keys collect (list :key key :year (bibtex-completion-get-value "year" (bibtex-completion-get-entry key)))) (if descending (lambda (x y) (> (string-to-number (plist-get x :year)) (string-to-number (plist-get y :year)))) (lambda (x y) (< (string-to-number (plist-get x :year)) (string-to-number (plist-get y :year)))))) collect (plist-get entry :key)))) ;; now render it (setq cite-string (concat (format "\\%s" (plist-get data :cite-type)) (or (plist-get data :optional-1) "") (or (plist-get data :optional-2) "") (format "{%s}" (string-join (plist-get data :keys) ",")))) (cl--set-buffer-substring (plist-get data :cite-begin) (plist-get data :cite-end) cite-string) (search-forward (car (plist-get data :keys))) (goto-char (match-beginning 0)))) (provide 'org-ref-latex) ;;; org-ref-latex.el ends here