;;; reftex-dcr.el --- viewing cross references and citations with RefTeX  -*- lexical-binding: t; -*-

;; Copyright (C) 1997-2025 Free Software Foundation, Inc.

;; Author: Carsten Dominik <dominik@science.uva.nl>
;; Maintainer: auctex-devel@gnu.org

;; This file is part of GNU Emacs.

;; GNU Emacs 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.

;; GNU Emacs 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;;; Code:

(eval-when-compile (require 'cl-lib))

(declare-function bibtex-beginning-of-entry "bibtex" ())

(require 'reftex)

;;;###autoload
(defun reftex-view-crossref (&optional arg auto-how fail-quietly)
  "View cross reference of macro at point.

Point must be on the KEY argument.  When at a `\\ref' macro, show
corresponding `\\label' definition, also in external
documents (`xr').  When on a label, show a locations where KEY is
referenced.  Subsequent calls find additional locations.  When on
a `\\cite', show the associated `\\bibitem' macro or the BibTeX
database entry.  When on a `\\bibitem', show a `\\cite' macro
which uses this KEY.  When on an `\\index', show other locations
marked by the same index entry.

To define additional cross referencing items, use the option
`reftex-view-crossref-extra'.  See also `reftex-view-crossref-from-bibtex'.
With one or two \\[universal-argument] prefixes, enforce rescanning of the document.
With argument 2, select the window showing the cross reference.
AUTO-HOW is only for the automatic crossref display and is handed through
to the functions `reftex-view-cr-cite' and `reftex-view-cr-ref'."

  (interactive "P")
  ;; See where we are.
  (let* ((macro (car (reftex-what-macro-safe 1)))
         (key (reftex-this-word "^{}%\n\r, \t"))
         dw)

    (if (or (null macro) (reftex-in-comment))
	(or fail-quietly
	    (error "Not on a crossref macro argument"))

      (setq reftex-call-back-to-this-buffer (current-buffer))

      (cond
       ((string-match "\\`\\\\cite\\|cite\\([s*]\\|texts?\\)?\\'\\|bibentry" macro)
	;; A citation macro: search for bibitems or BibTeX entries.
        ;; Match also commands from biblatex ending with `s'
        ;; (\parencites) or `*' (\parencite*) and `texts?'
        ;; (\footcitetext and \footcitetexts).
	(setq dw (reftex-view-cr-cite arg key auto-how)))
       ((string-match "\\`\\\\ref\\|ref\\(range\\|s\\)?\\*?\\'" macro)
	;; A reference macro: search for labels.
        ;; Match also commands from cleveref ending with `s' (\namecrefs).
	(setq dw (reftex-view-cr-ref arg key auto-how)))
       (auto-how nil)  ;; No further action for automatic display (speed)
       ((or (equal macro "\\label")
	    (member macro reftex-macros-with-labels))
	;; A label macro: search for reference macros
	(reftex-access-scan-info arg)
	(setq dw (reftex-view-regexp-match
		  (format reftex-find-reference-format (regexp-quote key))
		  4 nil nil)))
       ((equal macro "\\bibitem")
	;; A bibitem macro: search for citations
	(reftex-access-scan-info arg)
	(setq dw (reftex-view-regexp-match
		  (format reftex-find-citation-regexp-format (regexp-quote key))
		  4 nil nil)))
       ((member macro reftex-macros-with-index)
	(reftex-access-scan-info arg)
	(setq dw (reftex-view-regexp-match
		  (format reftex-find-index-entry-regexp-format
			  (regexp-quote key))
		  3 nil nil)))
       (t
	(reftex-access-scan-info arg)
	(catch 'exit
	  (let ((list reftex-view-crossref-extra)
		entry mre action group)
	    (while (setq entry (pop list))
	      (setq mre (car entry)
		    action (nth 1 entry)
		    group (nth 2 entry))
	      (when (string-match mre macro)
		(setq dw (reftex-view-regexp-match
			  (format action key) group nil nil))
		(throw 'exit t))))
	  (error "Not on a crossref macro argument"))))
      (if (and (eq arg 2) (windowp dw)) (select-window dw)))))

(defun reftex-view-cr-cite (arg key how)
  ;; View cross-reference of a ref cite.  HOW can have the values
  ;; nil:         Show in another window.
  ;; echo:        Show one-line info in echo area.
  ;; tmp-window:  Show in small window and arrange for window to disappear.

  ;; Ensure access to scanning info
  (reftex-access-scan-info (or arg current-prefix-arg))

  (if (eq how 'tmp-window)
      ;; Remember the window configuration
      (put 'reftex-auto-view-crossref 'last-window-conf
           (current-window-configuration)))

  (let (files size item (pos (point)) (win (selected-window)) pop-win
              (bibtype (reftex-bib-or-thebib)))
    ;; Find the citation mode and the file list
    (cond
;     ((assq 'bib (symbol-value reftex-docstruct-symbol))
     ((eq bibtype 'bib)
      (setq item nil
            files (reftex-get-bibfile-list)))
;     ((assq 'thebib (symbol-value reftex-docstruct-symbol))
     ((eq bibtype 'thebib)
      (setq item t
            files (reftex-uniquify
                   (mapcar #'cdr
                           (reftex-all-assq
                            'thebib (symbol-value reftex-docstruct-symbol))))))
     (reftex-default-bibliography
      (setq item nil
            files (reftex-default-bibliography)))
     (how)  ;; don't throw for special display
     (t (error "Cannot display crossref")))

    (if (eq how 'echo)
        ;; Display in Echo area
        (reftex-echo-cite key files item)
      ;; Display in a window
      (if (not (eq how 'tmp-window))
          ;; Normal display
          (reftex-pop-to-bibtex-entry key files nil t item)
        ;; A temporary window
        (condition-case nil
            (reftex-pop-to-bibtex-entry key files nil t item)
          (error (goto-char pos)
                 (message "cite: no such citation key %s" key)
                 (error "")))
        ;; Resize the window
        (setq size (max 1 (count-lines (point)
                                       (reftex-end-of-bib-entry item))))
        (let ((window-min-height 2))
          (shrink-window (1- (- (window-height) size)))
          (recenter 0))
        ;; Arrange restoration
        (add-hook 'pre-command-hook #'reftex-restore-window-conf))

        ;; Normal display in other window
      (add-hook 'pre-command-hook #'reftex-highlight-shall-die)
      (setq pop-win (selected-window))
      (select-window win)
      (goto-char pos)
      (when (equal arg 2)
        (select-window pop-win)))))

(defun reftex-view-cr-ref (arg label how)
  ;; View cross-reference of a ref macro.  HOW can have the values
  ;; nil:         Show in another window.
  ;; echo:        Show one-line info in echo area.
  ;; tmp-window:  Show in small window and arrange for window to disappear.

  ;; Ensure access to scanning info
  (reftex-access-scan-info (or arg current-prefix-arg))

  (if (eq how 'tmp-window)
      ;; Remember the window configuration
      (put 'reftex-auto-view-crossref 'last-window-conf
           (current-window-configuration)))

  (let* ((xr-data (assoc 'xr (symbol-value reftex-docstruct-symbol)))
         (xr-re (nth 2 xr-data))
         (entry (assoc label (symbol-value reftex-docstruct-symbol)))
         (win (selected-window)) pop-win (pos (point)))

    (if (and (not entry) (stringp label) xr-re (string-match xr-re label))
        ;; Label is defined in external document
        (save-excursion
          (save-match-data
            (set-buffer
             (or (reftex-get-file-buffer-force
                  (cdr (assoc (match-string 1 label) (nth 1
                                                          xr-data))))
                 (error "Problem with external label %s" label))))
          (setq label (substring label (match-end 1)))
          (reftex-access-scan-info)
          (setq entry
                (assoc label (symbol-value reftex-docstruct-symbol)))))
    (if (eq how 'echo)
        ;; Display in echo area
        (reftex-echo-ref label entry (symbol-value reftex-docstruct-symbol))
      (let ((window-conf (current-window-configuration)))
        (condition-case nil
            (reftex-show-label-location entry t nil t t)
          (error (set-window-configuration window-conf)
                 (message "ref: Label %s not found" label)
                 (error "ref: Label %s not found" label)))) ;; 2nd is line OK
      (add-hook 'pre-command-hook #'reftex-highlight-shall-die)

      (when (eq how 'tmp-window)
        ;; Resize window and arrange restoration
        (shrink-window (1- (- (window-height) 9)))
        (recenter '(4))
        (add-hook 'pre-command-hook #'reftex-restore-window-conf))
      (setq pop-win (selected-window))
      (select-window win)
      (goto-char pos)
      (when (equal arg 2)
        (select-window pop-win)))))

;;;###autoload
(defun reftex-mouse-view-crossref (ev)
  "View cross reference of \\ref or \\cite macro where you click.
If the macro at point is a \\ref, show the corresponding label definition.
If it is a \\cite, show the BibTeX database entry.
If there is no such macro at point, search forward to find one.
With argument, actually select the window showing the cross reference."
  (interactive "e")
  ;; Make sure the referencing macro stays visible in the original window.
  (mouse-set-point ev)
  (reftex-view-crossref current-prefix-arg))

(defun reftex-view-crossref-when-idle ()
  "Display info about crossref at point in echo area or a window.
This function is designed to work with an idle timer and returns quickly
if the call is useless."
  (and reftex-mode
       ;; Make sure message area is free if we need it.
       (or (eq reftex-auto-view-crossref 'window) (not (current-message)))
       ;; Make sure we are not already displaying this one
       (not (memq last-command '(reftex-view-crossref
                                 reftex-mouse-view-crossref)))
       ;; Quick precheck if this might be a relevant spot
       ;; `reftex-view-crossref' will do a more thorough check.
       (save-excursion
         (search-backward "\\" nil t)
         (looking-at "\\\\[a-zA-Z]*\\(cite\\|ref\\|bibentry\\)"))
       ;; Also check if point is inside a mandatory argument where the
       ;; cite/ref key usually resides: (bug#38258)
       (save-excursion
         (condition-case nil
             (let ((forward-sexp-function nil))
               (up-list -1)
               (= (following-char) ?\{))
           (error nil)))
       ;; Finally, call `reftex-view-crossref':
       (condition-case nil
           (let ((current-prefix-arg nil))
             (cond
              ((eq reftex-auto-view-crossref t)
               (reftex-view-crossref -1 'echo 'quiet))
              ((eq reftex-auto-view-crossref 'window)
               (reftex-view-crossref -1 'tmp-window 'quiet))
              (t nil)))
         (error nil))))

(defun reftex-restore-window-conf ()
  (set-window-configuration (get 'reftex-auto-view-crossref 'last-window-conf))
  (put 'reftex-auto-view-crossref 'last-window-conf nil)
  (remove-hook 'pre-command-hook #'reftex-restore-window-conf))

(defun reftex-echo-ref (label entry docstruct)
  ;; Display crossref info in echo area.
  (cond
   ((null docstruct)
    (message "%s"
	     (substitute-command-keys (format reftex-no-info-message "ref"))))
   ((null entry)
    (message "ref: unknown label: %s" label))
   (t
    (when (stringp (nth 2 entry))
      (message "ref(%s): %s" (nth 1 entry) (nth 2 entry)))
    (let ((buf (get-buffer " *Echo Area*")))
      (when buf
        (with-current-buffer buf
          (run-hooks 'reftex-display-copied-context-hook)))))))

(defun reftex-echo-cite (key files item)
  ;; Display citation info in echo area.
  (let* ((cache (assq 'bibview-cache (symbol-value reftex-docstruct-symbol)))
         (cache-entry (assoc key (cdr cache)))
         entry string buf (all-files files))

    (if (and reftex-cache-cite-echo cache-entry)
        ;; We can just use the cache
        (setq string (cdr cache-entry))

      ;; Need to look in the database
      (unless reftex-revisit-to-echo
        (setq files (reftex-visited-files files)))

      (setq entry
            (condition-case nil
                (save-excursion
                  (reftex-pop-to-bibtex-entry key files nil nil item t))
              (error
               (if (and files (= (length all-files) (length files)))
                   (message "cite: no such database entry: %s" key)
                 (message "%s" (substitute-command-keys
				(format reftex-no-info-message "cite"))))
               nil)))
      (when entry
        (if item
            (setq string (reftex-nicify-text entry))
          (setq string (reftex-make-cite-echo-string
                        (reftex-parse-bibtex-entry entry)
                        reftex-docstruct-symbol)))))
    (unless (or (null string) (equal string ""))
      (message "cite: %s" string))
    (when (setq buf (get-buffer " *Echo Area*"))
      (with-current-buffer buf
        (run-hooks 'reftex-display-copied-context-hook)))))

;;;###autoload
(defun reftex-toggle-auto-view-crossref ()
  "Toggle the automatic display of crossref information in the echo area.
When active, leaving point idle in the argument of a \\ref or \\cite macro
will display info in the echo area."
  (interactive)
  (if reftex-auto-view-crossref-timer
      (progn
        (cancel-timer reftex-auto-view-crossref-timer)
        (setq reftex-auto-view-crossref-timer nil)
        (message "Automatic display of crossref information was turned off"))
    (setq reftex-auto-view-crossref-timer
          (run-with-idle-timer
           reftex-idle-time t #'reftex-view-crossref-when-idle))
    (unless reftex-auto-view-crossref
      (setq reftex-auto-view-crossref t))
    (message "Automatic display of crossref information was turned on")))

;;;###autoload
(defun reftex-view-crossref-from-bibtex (&optional arg)
  "View location in a LaTeX document which cites the BibTeX entry at point.
Since BibTeX files can be used by many LaTeX documents, this function
prompts upon first use for a buffer in RefTeX mode.  To reset this
link to a document, call the function with a prefix arg.
Calling this function several times find successive citation locations."
  (interactive "P")
  (when arg
    ;; Break connection to reference buffer
    (put 'reftex-bibtex-view-cite-locations :ref-buffer nil))
  (let ((ref-buffer (get 'reftex-bibtex-view-cite-locations :ref-buffer)))
    ;; Establish connection to reference buffer
    (unless ref-buffer
      (setq ref-buffer
            (save-current-buffer
              (completing-read
               "Reference buffer: "
               (delq nil
                     (mapcar
                      (lambda (b)
                        (set-buffer b)
                        (if reftex-mode (list (buffer-name b)) nil))
                      (buffer-list)))
               nil t)))
      (put 'reftex-bibtex-view-cite-locations :ref-buffer ref-buffer))
    ;; Search for citations
    (bibtex-beginning-of-entry)
    (if (looking-at
         "@[a-zA-Z]+[ \t\n\r]*[{(][ \t\n\r]*\\([^, \t\r\n}]+\\)")
        (progn
          (goto-char (match-beginning 1))
          (reftex-view-regexp-match
           (format reftex-find-citation-regexp-format
                   (regexp-quote (match-string 1)))
           4 arg ref-buffer))
      (error "Cannot find citation key in BibTeX entry"))))

(defun reftex-view-regexp-match (re &optional highlight-group new ref-buffer)
  ;; Search for RE in current document or in the document of REF-BUFFER.
  ;; Continue the search, if the same re was searched last.
  ;; Highlight the group HIGHLIGHT-GROUP of the match.
  ;; When NEW is non-nil, start a new search regardless.
  ;; Match point is displayed in another window.
  ;; Upon success, returns the window which displays the match.

  ;;; Decide if new search or continued search
  (let* ((oldprop (get 'reftex-view-regexp-match :props))
         (newprop (list (current-buffer) re))
         (cont (and (not new) (equal oldprop newprop)))
         (cnt (if cont (get 'reftex-view-regexp-match :cnt) 0))
         (current-window (selected-window))
         (window-conf (current-window-configuration))
         match pop-window)
    (switch-to-buffer-other-window (or ref-buffer (current-buffer)))
    ;; Search
    (condition-case nil
        (if cont
            (setq match (reftex-global-search-continue))
          (reftex-access-scan-info)
          (setq match (reftex-global-search re (reftex-all-document-files))))
      (error nil))
    ;; Evaluate the match.
    (if match
        (progn
          (put 'reftex-view-regexp-match :props newprop)
          (put 'reftex-view-regexp-match :cnt (cl-incf cnt))
          (reftex-highlight 0 (match-beginning highlight-group)
                            (match-end highlight-group))
          (add-hook 'pre-command-hook #'reftex-highlight-shall-die)
          (setq pop-window (selected-window)))
      (put 'reftex-view-regexp-match :props nil)
      (or cont (set-window-configuration window-conf)))
    (select-window current-window)
    (if match
        (progn
          (message "Match Nr. %s" cnt)
          pop-window)
      (if cont
          (error "No further matches (total number of matches: %d)" cnt)
        (error "No matches")))))

(defvar reftex-global-search-marker (make-marker))
(defun reftex-global-search (regexp file-list)
  ;; Start a search for REGEXP in all files of FILE-LIST
  (put 'reftex-global-search :file-list file-list)
  (put 'reftex-global-search :regexp regexp)
  (move-marker reftex-global-search-marker nil)
  (reftex-global-search-continue))

(defun reftex-global-search-continue ()
  ;; Continue a global search started with `reftex-global-search'
  (unless (get 'reftex-global-search :file-list)
    (error "No global search to continue"))
  (let* ((file-list (get 'reftex-global-search :file-list))
         (regexp (get 'reftex-global-search :regexp))
         (buf (or (marker-buffer reftex-global-search-marker)
                  (reftex-get-file-buffer-force (car file-list))))
         (pos (or (marker-position reftex-global-search-marker) 1))
         file)
    ;; Take up starting position
    (unless buf (error "No such buffer %s" buf))
    (switch-to-buffer buf)
    (widen)
    (goto-char pos)
    ;; Search and switch file if necessary
    (if (catch 'exit
          (while t
            (when (re-search-forward regexp nil t)
              (move-marker reftex-global-search-marker (point))
              (throw 'exit t))
            ;; No match - goto next file
            (pop file-list)
            (or file-list (throw 'exit nil))
            (setq file (car file-list)
                  buf (reftex-get-file-buffer-force file))
            (unless buf (error "Cannot access file %s" file))
            (put 'reftex-global-search :file-list file-list)
            (switch-to-buffer buf)
            (widen)
            (goto-char 1)))
        t
      (move-marker reftex-global-search-marker nil)
      (error "All files processed"))))

(provide 'reftex-dcr)

;;; reftex-dcr.el ends here

;; Local Variables:
;; generated-autoload-file: "reftex-loaddefs.el"
;; End:
