;;; org-ref-ivy-cite.el --- Use ivy for completion in org-ref -*- lexical-binding: t; -*- ;; Copyright (C) 2016 John Kitchin ;; Author: John Kitchin ;; Keywords: ;; 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: ;; (declare-function 'org-ref-insert-key-at-point "org-ref-core.el") (declare-function 'org-ref-find-bibliography "org-ref-core.el") (declare-function 'org-ref-get-labels "org-ref-core.el") (declare-function 'org-ref-get-bibtex-key-and-file "org-ref-core.el") ;;; Code: (require 'ivy) (require 'org-ref-bibtex) (require 'org-ref-citeproc) (require 'bibtex-completion) ;; This lets you customize how the completion for ivy is displayed. The default ;; is in the minibuffer. You may like to see something more like a popup though. (defcustom org-ref-ivy-display-function nil "ivy function to display completion with. Set to `ivy-display-function-overlay' to get popups at point." :type 'function :group 'org-ref) (when org-ref-ivy-display-function (add-to-list 'ivy-display-functions-alist `(org-ref-ivy-insert-cite-link . ,org-ref-ivy-display-function)) (add-to-list 'ivy-display-functions-alist `(org-ref-ivy-insert-label-link . ,org-ref-ivy-display-function)) (add-to-list 'ivy-display-functions-alist `(org-ref-ivy-insert-ref-link . ,org-ref-ivy-display-function))) (defvar org-ref-cite-types) (defvar org-ref-show-citation-on-enter) (defvar org-ref-ivy-cite-marked-candidates '() "Holds entries marked in `org-ref-ivy-insert-cite-link'.") ;;;###autoload (defun org-ref-ivy-cite-completion () "Use ivy for completion." (interactive) ;; Define core functions for org-ref (setq org-ref-insert-link-function 'org-ref-insert-link org-ref-insert-cite-function 'org-ref-ivy-insert-cite-link org-ref-insert-label-function 'org-ref-ivy-insert-label-link org-ref-insert-ref-function 'org-ref-ivy-insert-ref-link org-ref-cite-onclick-function (lambda (_) (org-ref-cite-hydra/body)))) (org-ref-ivy-cite-completion) (define-key org-mode-map (kbd org-ref-insert-cite-key) org-ref-insert-link-function) (defun or-looking-forward-cite () "Return if point is in the position before a citation." (save-excursion (forward-char) (-contains? org-ref-cite-types (org-element-property :type (org-element-context))))) (defun or-looking-back-cite () "Return if point is in the position after a citation." (save-excursion (forward-char -1) (-contains? org-ref-cite-types (org-element-property :type (org-element-context))))) (defun or-ivy-bibtex-insert-cite (entry) "Insert a citation for ENTRY. If `org-ref-ivy-cite-marked-candidates' is non-nil then they are added instead of ENTRY. ENTRY is selected from `orhc-bibtex-candidates'." (with-ivy-window (if org-ref-ivy-cite-marked-candidates (cl-loop for entry in org-ref-ivy-cite-marked-candidates do (if ivy-current-prefix-arg (let ((org-ref-default-citation-link (ivy-read "Type: " org-ref-cite-types))) (org-ref-insert-key-at-point (list (cdr (assoc "=key=" entry))))) (org-ref-insert-key-at-point (list (cdr (assoc "=key=" entry)))))) (if ivy-current-prefix-arg (let ((org-ref-default-citation-link (ivy-read "Type: " org-ref-cite-types))) (org-ref-insert-key-at-point (list (cdr (assoc "=key=" entry))))) (org-ref-insert-key-at-point (list (cdr (assoc "=key=" entry)))))))) (defun or-ivy-bibtex-open-pdf (entry) "Open the pdf associated with ENTRY. ENTRY is selected from `orhc-bibtex-candidates'." (with-ivy-window (let ((pdf (expand-file-name (format "%s.pdf" (cdr (assoc "=key=" entry))) org-ref-pdf-directory))) (if (file-exists-p pdf) (org-open-file pdf) (message "No pdf found for %s" (cdr (assoc "=key=" entry))))))) (defun or-ivy-bibtex-open-notes (entry) "Open the notes associated with ENTRY. ENTRY is selected from `orhc-bibtex-candidates'." (with-ivy-window (org-ref-open-notes-at-point (cdr (assoc "=key=" entry))))) (defun or-ivy-bibtex-open-entry (entry) "Open the bibtex file at ENTRY. ENTRY is selected from `orhc-bibtex-candidates'." (find-file (cdr (assoc "bibfile" entry))) (goto-char (cdr (assoc "position" entry))) (bibtex-beginning-of-entry)) (defun or-ivy-bibtex-copy-entry (entry) "Copy selected bibtex ENTRY to the clipboard." (with-temp-buffer (save-window-excursion (or-ivy-bibtex-open-entry entry) (bibtex-copy-entry-as-kill)) (bibtex-yank) (kill-region (point-min) (point-max)))) (defun or-ivy-bibtex-open-url (entry) "Open the URL associated with ENTRY. ENTRY is selected from `orhc-bibtex-candidates'." (let ((url (cdr (assoc "url" entry)))) (if url (browse-url url) (message "No url found for %s" (cdr (assoc "=key=" entry)))))) (defun or-ivy-bibtex-open-doi (entry) "Open the DOI associated with ENTRY. ENTRY is selected from `orhc-bibtex-candidates'." (let ((doi (cdr (assoc "doi" entry)))) (if doi (browse-url (format "http://dx.doi.org/%s" doi)) (message "No doi found for %s" (cdr (assoc "=key=" entry)))))) (defun or-ivy-bibtex-set-keywords (entry) "Prompt for keywords, and put them on the selected ENTRY." (let ((keywords (read-string "Keyword(s) comma-separated: " )) entry-keywords) (save-window-excursion (or-ivy-bibtex-open-entry entry) (setq entry-keywords (bibtex-autokey-get-field "keywords")) (bibtex-set-field "keywords" (if (> (length entry-keywords) 0) (concat entry-keywords ", " keywords) keywords))))) (defun or-ivy-bibtex-email-entry (entry) "Insert selected ENTRY and attach pdf file to an email. Create email unless called from an email." (with-ivy-window (let ((goto-to nil)) (unless (memq major-mode '(message-mode mu4e-compose-mode)) (setq goto-to t) (compose-mail) (message-goto-body)) (save-window-excursion (or-ivy-bibtex-open-entry entry) (bibtex-copy-entry-as-kill)) (insert (pop bibtex-entry-kill-ring)) (insert "\n") (let ((pdf (expand-file-name (format "%s.pdf" (cdr (assoc "=key=" entry))) org-ref-pdf-directory))) (if (file-exists-p pdf) (mml-attach-file pdf))) (when goto-to (message-goto-to))))) (defun or-ivy-bibtex-formatted-citation (entry) "Return string containing formatted citations for ENTRY. This uses a citeproc library." (let ((enable-recursive-minibuffers t)) (ivy-read "Style: " '("unsrt" "author-year") :action 'load-library :require-match t :preselect "unsrt" :caller 'or-ivy-formatted-citation) (format "%s\n\n" (orhc-formatted-citation entry)))) (defun or-ivy-bibtex-insert-formatted-citation (entry) "Insert formatted citations at point for selected entries." (with-ivy-window (insert (mapconcat 'identity (cl-loop for entry in (or org-ref-ivy-cite-marked-candidates (list entry)) collect (org-ref-format-bibtex-entry entry)) "\n\n")))) (defun or-ivy-bibtex-copy-formatted-citation (entry) "Copy formatted citation to clipboard for ENTRY." (kill-new (org-ref-format-entry entry))) (defun or-ivy-bibtex-add-entry (_) "Open a bibliography file and move point to the end, in order to add a new bibtex entry. The arg is selected from `orhc-bibtex-candidates' but ignored." (ivy-read "bibtex file: " org-ref-bibtex-files :require-match t :action 'find-file :caller 'or-ivy-bibtex-add-entry) (widen) (goto-char (point-max)) (unless (bolp) (insert "\n"))) (defvar org-ref-ivy-cite-actions '(("b" or-ivy-bibtex-open-entry "Open bibtex entry") ("B" or-ivy-bibtex-copy-entry "Copy bibtex entry") ("p" or-ivy-bibtex-open-pdf "Open pdf") ("n" or-ivy-bibtex-open-notes "Open notes") ("u" or-ivy-bibtex-open-url "Open url") ("d" or-ivy-bibtex-open-doi "Open doi") ("k" or-ivy-bibtex-set-keywords "Add keywords") ("e" or-ivy-bibtex-email-entry "Email entry") ("f" or-ivy-bibtex-insert-formatted-citation "Insert formatted citation") ("F" or-ivy-bibtex-copy-formatted-citation "Copy formatted citation") ("a" or-ivy-bibtex-add-entry "Add bibtex entry")) "List of additional actions for `org-ref-ivy-insert-cite-link' (the default action being to insert a citation).") (defvar org-ref-ivy-cite-re-builder 'ivy--regex-ignore-order "Regex builder to use in `org-ref-ivy-insert-cite-link'. Can be set to nil to use Ivy's default).") (defun org-ref-swap (i j lst) "Swap index I and J in the list LST." (let ((tempi (nth i lst))) (setf (nth i lst) (nth j lst)) (setf (nth j lst) tempi)) lst) (defun org-ref-ivy-current () (if (boundp 'ivy--current) ivy--current (ivy-state-current ivy-last))) (defun org-ref-ivy-move-up () "Move ivy candidate up and update candidates." (interactive) (setf (ivy-state-collection ivy-last) (org-ref-swap ivy--index (1- ivy--index) (ivy-state-collection ivy-last))) (setf (ivy-state-preselect ivy-last) (org-ref-ivy-current)) (ivy--reset-state ivy-last)) (defun org-ref-ivy-move-down () "Move ivy candidate down." (interactive) (setf (ivy-state-collection ivy-last) (org-ref-swap ivy--index (1+ ivy--index) (ivy-state-collection ivy-last))) (setf (ivy-state-preselect ivy-last) (org-ref-ivy-current)) (ivy--reset-state ivy-last)) (defun org-ref-ivy-sort-year-ascending () "Sort entries by year in ascending order." (interactive) (setf (ivy-state-collection ivy-last) (cl-sort (copy-sequence (ivy-state-collection ivy-last)) (lambda (a b) (let ((y1 (string-to-number (or (cdr (assoc "year" a)) "0"))) (y2 (string-to-number (or (cdr (assoc "year" b)) "0")))) (< y1 y2))))) (setf (ivy-state-preselect ivy-last) (org-ref-ivy-current)) (ivy--reset-state ivy-last)) (defun org-ref-ivy-sort-year-descending () "sort entries by year in descending order." (interactive) (setf (ivy-state-collection ivy-last) (cl-sort (copy-sequence (ivy-state-collection ivy-last)) (lambda (a b) (let ((y1 (string-to-number (or (cdr (assoc "year" a)) "0"))) (y2 (string-to-number (or (cdr (assoc "year" b)) "0")))) (> y1 y2))))) (setf (ivy-state-preselect ivy-last) (org-ref-ivy-current)) (ivy--reset-state ivy-last)) ;; * marking candidates (defun org-ref-ivy-mark-candidate () "Add current candidate to `org-ref-ivy-cite-marked-candidates'. If candidate is already in, remove it." (interactive) (let ((cand (or (assoc (org-ref-ivy-current) (ivy-state-collection ivy-last)) (org-ref-ivy-current)))) (if (-contains? org-ref-ivy-cite-marked-candidates cand) ;; remove it from the marked list (setq org-ref-ivy-cite-marked-candidates (-remove-item cand org-ref-ivy-cite-marked-candidates)) ;; add to list (setq org-ref-ivy-cite-marked-candidates (append org-ref-ivy-cite-marked-candidates (list cand))))) (ivy-next-line)) (defun org-ref-ivy-show-marked-candidates () "Show marked candidates." (interactive) (setf (ivy-state-collection ivy-last) org-ref-ivy-cite-marked-candidates) (setf (ivy-state-preselect ivy-last) (org-ref-ivy-current)) (ivy--reset-state ivy-last)) (defun org-ref-ivy-show-all () "Show all the candidates." (interactive) (setf (ivy-state-collection ivy-last) (orhc-bibtex-candidates)) (ivy--reset-state ivy-last)) ;; * org-ref-cite keymap (defvar org-ref-ivy-cite-keymap (let ((map (make-sparse-keymap))) (define-key map (kbd "C-") 'org-ref-ivy-mark-candidate) (define-key map (kbd "C-,") 'org-ref-ivy-show-marked-candidates) (define-key map (kbd "C-.") 'org-ref-ivy-show-all) (define-key map (kbd "C-") 'org-ref-ivy-move-up) (define-key map (kbd "C-") 'org-ref-ivy-move-down) (define-key map (kbd "C-y") 'org-ref-ivy-sort-year-ascending) (define-key map (kbd "C-M-y") 'org-ref-ivy-sort-year-descending) (define-key map (kbd "C-k") (lambda () (interactive) (beginning-of-line) (kill-visual-line) (setf (ivy-state-collection ivy-last) (orhc-bibtex-candidates)) (setf (ivy-state-preselect ivy-last) (org-ref-ivy-current)) (ivy--reset-state ivy-last))) (define-key map (kbd "C-") (lambda () "Apply action and move to next/previous candidate." (interactive) (ivy-call) (ivy-next-line))) ;; (define-key ivy-minibuffer-map (kbd "M-") ;; (lambda () ;; "Apply default action to all marked candidates." ;; (interactive) ;; (mapc (ivy--get-action ivy-last) ;; org-ref-ivy-cite-marked-candidates) ;; (ivy-exit-with-action (function (lambda (_) nil))))) map) "A key map for `org-ref-ivy-insert-cite-link'.") (ivy-set-actions 'org-ref-ivy-insert-cite-link org-ref-ivy-cite-actions) (defun org-ref-ivy-insert-cite-link (&optional arg) "ivy function for interacting with bibtex. Uses `org-ref-find-bibliography' for bibtex sources, unless a prefix ARG is used, which uses `org-ref-default-bibliography'." (interactive "P") (setq org-ref-bibtex-files (if arg org-ref-default-bibliography (org-ref-find-bibliography))) (setq org-ref-ivy-cite-marked-candidates '()) (ivy-read "Open: " (orhc-bibtex-candidates) :require-match t :keymap org-ref-ivy-cite-keymap :re-builder org-ref-ivy-cite-re-builder :action 'or-ivy-bibtex-insert-cite :caller 'org-ref-ivy-insert-cite-link)) (defun org-ref-ivy-cite-transformer (s) "Make entry red if it is marked." (let* ((fill-column (frame-width)) (fill-prefix " ") (wrapped-s (with-temp-buffer (insert s) (fill-paragraph) (buffer-string)))) (if (-contains? (if (listp (car org-ref-ivy-cite-marked-candidates)) (mapcar 'car org-ref-ivy-cite-marked-candidates) org-ref-ivy-cite-marked-candidates) s) (propertize wrapped-s 'face 'font-lock-warning-face) (propertize wrapped-s 'face nil)))) (ivy-set-display-transformer 'org-ref-ivy-insert-cite-link 'org-ref-ivy-cite-transformer ) (defun org-ref-ivy-insert-label-link () "Insert a label with ivy." (interactive) (insert (concat (if (not (looking-back "label:" 6)) "label:" "") (ivy-read "label: " (org-ref-get-labels) :caller 'org-ref-ivy-insert-label-link)))) (defun org-ref-ivy-insert-ref-link () "Insert a ref link with ivy. Use a prefix arg to select the ref type." (interactive) (let ((label (ivy-read "label: " (org-ref-get-labels) :require-match t :caller 'org-ref-ivy-insert-ref-link))) (cond ;; from a colon insert ((looking-back ":" 1) (insert label)) ;; non-default (ivy-current-prefix-arg (insert (ivy-read "type: " org-ref-ref-types) ":" label)) ;; default (t (insert (or (when (looking-at "$") " ") "") (concat org-ref-default-ref-type ":" label)))))) (require 'hydra) (setq hydra-is-helpful t) (defhydra org-ref-cite-hydra (:color blue) " _p_: Open pdf _w_: WOS _g_: Google Scholar _K_: Copy citation to clipboard _u_: Open url _r_: WOS related _P_: Pubmed _k_: Copy key to clipboard _n_: Open notes _c_: WOS citing _C_: Crossref _f_: Copy formatted entry _o_: Open entry _e_: Email entry ^ ^ _q_: quit _i_: Insert cite _h_: change type " ("o" org-ref-open-citation-at-point nil) ("p" org-ref-open-pdf-at-point nil) ("n" org-ref-open-notes-at-point nil) ("u" org-ref-open-url-at-point nil) ("w" org-ref-wos-at-point nil) ("r" org-ref-wos-related-at-point nil) ("c" org-ref-wos-citing-at-point nil) ("g" org-ref-google-scholar-at-point nil) ("P" org-ref-pubmed-at-point nil) ("C" org-ref-crossref-at-point nil) ("K" org-ref-copy-entry-as-summary nil) ("k" (progn (kill-new (car (org-ref-get-bibtex-key-and-file)))) nil) ("f" (kill-new (org-ref-format-entry (org-ref-get-bibtex-key-under-cursor))) nil) ("e" (kill-new (save-excursion (org-ref-open-citation-at-point) (org-ref-email-bibtex-entry))) nil) ("i" (funcall org-ref-insert-cite-function)) ("h" org-ref-change-cite-type) ("q" nil)) (defun org-ref-ivy-onclick-actions () "An alternate click function that uses ivy for action selection. Each action is taken from `org-ref-ivy-cite-actions'. Each action should act on a bibtex entry that matches the key in `orhc-bibtex-candidates'. Set `org-ref-cite-onclick-function' to this function to use it." (interactive) (ivy-read "action: " (cl-loop for i from 0 for (_ func s) in org-ref-ivy-cite-actions collect (cons (format "%2s. %s" i s) func)) :action (lambda (f) (let* ((key (car (org-ref-get-bibtex-key-and-file))) (entry (cdr (elt (orhc-bibtex-candidates) (-elem-index key (cl-loop for entry in (orhc-bibtex-candidates) collect (cdr (assoc "=key=" entry )))))))) (funcall f entry))))) ;; * org-ref-ivy-set-keywords (defvar org-ref-ivy-set-keywords-keymap (let ((map (make-sparse-keymap))) (define-key map (kbd "C-") 'org-ref-ivy-mark-candidate) (define-key map (kbd "C-,") 'org-ref-ivy-show-marked-candidates) (define-key map (kbd "C-.") 'org-ref-ivy-show-all) (define-key map (kbd "C-") 'org-ref-ivy-move-up) (define-key map (kbd "C-") 'org-ref-ivy-move-down) map) "A key map for `org-ref-ivy-set-keywords'.") (defun org-ref-ivy-set-keywords () "Add keywords to bibtex entries selected by org-ref-ivy." (interactive) (setq org-ref-ivy-cite-marked-candidates '()) (ivy-read "Keywords: " (org-ref-bibtex-keywords) :keymap org-ref-ivy-set-keywords-keymap :caller 'org-ref-ivy-set-keywords :action (lambda (key) (org-ref-set-bibtex-keywords (mapconcat 'identity (or org-ref-ivy-cite-marked-candidates (list key)) ", "))))) (ivy-set-display-transformer 'org-ref-ivy-set-keywords 'org-ref-ivy-cite-transformer) (provide 'org-ref-ivy-cite) ;;; org-ref-ivy-cite.el ends here