Using prefix args in ivy actions

| categories: ivy, emacs | tags:

Table of Contents

There is a brand new feature in ivy which allows you to pass prefix args to the action functions. This change has made it into MELPA by now, so you can try it out. Check out this 1 for an emacs -Q setup that I used for this post. This installs ivy and org-ref with some minimal setup.

The video for this post can be found here: https://www.youtube.com/watch?v=Y8HHLAE_-yA

In this post I will show how to use this new feature to create an ivy selection function that inserts a citation from a bibtex file, and with a prefix arg lets you choose the type of citation to insert.

org-ref provides a function that generates candidates for selection. Each candidate is a list where the car of the list is a display string, and the cdr is an a-list of properties. I have a lot of entries in here, so it is important to have a convenient selection tool.

(setq org-ref-bibtex-files '("references.bib"))
(length (orhc-bibtex-candidates))

Here is an example of the first entry in my bibliography. We will need to extract the key from that.

(elt (orhc-bibtex-candidates) 0)

Here is the key from that entry.

(cdr (assoc "=key=" (elt (orhc-bibtex-candidates) 0)))

By default we will insert that as kitchin-2015-examp but there are other types of citations we might use too like kitchin-2015-examp. org-ref provides a list of citation types we could insert. Here they are. This somewhat complicated code just wraps the string so it fits in the blog post nicely.

(with-temp-buffer 
  (insert (format "%s" org-ref-cite-types))
  (fill-region (point-min) (point-max))
  (buffer-string))

So, we are now prepared to make a simple ivy function to query our bibliography that has a default action to insert a standard citation, but we can use a prefix to change the citation type. The prefix arg is stored in the global variable ivy-current-prefix-arg which can be checked inside the action function. We can check for it in the action function and do something different if a prefix arg is used. Here is the function.

(defun cite ()
  (interactive)
  (ivy-read "select: " (orhc-bibtex-candidates)
            :action (lambda (entry) 
                      (let ((key (cdr (assoc "=key=" entry)))
                            (type (if ivy-current-prefix-arg
                                      (ivy-read "type: " org-ref-cite-types)
                                    "cite")))
                        (with-ivy-window
                          (insert (format "%s:%s" type key)))))))

To get the default action, we run M-x cite, enter our query, select an entry and press return. To get an alternate cite type, we run M-x cite, enter the query, select an entry, then type C-u return, which will prompt you for an alternate citation type, then insert your choice and the citation. Here are some examples. kitchin-2015-examp kitchin-2015-examp kitchin-2015-examp

In summary, these aren't functions you would want to use; they don't handle a lot of the nuances of multiple citations. They are just to illustrate in a pretty simple way how easy it is to use a prefix arg in an ivy action function now!

1 Bare bones setup

This will setup the bare bones emacs that I used for this post.

(setq package-user-dir (expand-file-name "sandbox"))

(setq package-archives
      '(("melpa" . "http://melpa.org/packages/")))

(require 'package)

;;; Code:

(package-initialize)

(unless (package-installed-p 'use-package)
  (package-refresh-contents)
  (package-install 'use-package))

(eval-when-compile
  (require 'use-package))

(setq use-package-always-ensure t)

(use-package ivy)

(use-package org-ref
 :init 
 (setq org-ref-default-bibliography '("~/Dropbox/bibliography/references.bib"))
 :config (require 'org-ref-helm-cite))

(global-visual-line-mode 1)
(setq org-confirm-babel-evaluate nil)
(load-theme 'leuven)

Copyright (C) 2016 by John Kitchin. See the License for information about copying.

org-mode source

Org-mode version = 8.3.4

Discuss on Twitter

Dynamic sorting with ivy

| categories: ivy, emacs | tags:

I have been exploring ivy a lot these days as a general purpose completion backend. One need I have is dynamic resorting of candidates. I illustrate how to achieve that here. A big thanks to Oleh Krehel (author of ivy) for a lot help today getting this working!

You may want to check out the video: https://www.youtube.com/watch?v=nFKfM3MOAd0

First, a typical ivy-read example. Below I have a set of contact data for some people, and have setup an ivy-read command that inserts the email in the current buffer by default, and a second action for the phone. What is missing that I would like to do is dynamically reorder the candidates, including sorting all the candidates, swapping candidates up and down to fine tune the order, and then finally applying an action to all the candidates.

(defun ct ()
  (interactive)
  (ivy-read "contact: " '(("Kno Body" "kb@true.you" "555-1212")
                          ("A. Person" "ap@some.come" "867-5304")
                          ("G. Willikers" "gw@not.me" "555-5555"))
            :action '(1
                      ("o" (lambda (x)
                             (with-ivy-window
                               (insert
                                (if (not (looking-back " ")) ", " "")
                                (elt x 0))))
                       "insert email")
                      ("p" (lambda (x)
                             (with-ivy-window
                               (insert
                                (if (not (looking-back " ")) ", " "")
                                (elt x 1))))
                       "insert phone"))))

So, first a set of functions to manipulate the candidates. We create a swap function, two functions to move candidates up and down, and two functions that sort the whole list of candidates in ascending and descending order. In each case, we just update the ivy collection with the new modified collection, we save the currently selected candidate, and then reset the state to update the candidates.

(defun 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 ivy-move-up ()
  "Move ivy candidate up."
  (interactive)
  (setf (ivy-state-collection ivy-last)
        (swap ivy--index (1- ivy--index) (ivy-state-collection ivy-last)))
  (setf (ivy-state-preselect ivy-last) ivy--current)
  (ivy--reset-state ivy-last))

(defun ivy-move-down ()
  "Move ivy candidate down."
  (interactive)
  (setf (ivy-state-collection ivy-last)
        (swap ivy--index (1+ ivy--index) (ivy-state-collection ivy-last)))
  (setf (ivy-state-preselect ivy-last) ivy--current)
  (ivy--reset-state ivy-last))

(defun ivy-a-z ()
  "Sort ivy candidates from a-z."
  (interactive)
  (setf (ivy-state-collection ivy-last)
        (cl-sort (ivy-state-collection ivy-last)
                 (if (listp (car (ivy-state-collection ivy-last)))
                     (lambda (a b)
                       (string-lessp (car a) (car b)))
                   (lambda (a b)
                     (string-lessp a b)))))
  (setf (ivy-state-preselect ivy-last) ivy--current)
  (ivy--reset-state ivy-last))

(defun ivy-z-a ()
  "Sort ivy candidates from z-a."
  (interactive)
  (setf (ivy-state-collection ivy-last)
        (cl-sort (ivy-state-collection ivy-last)
                 (if (listp (car (ivy-state-collection ivy-last)))
                     (lambda (a b)
                       (string-greaterp (car a) (car b)))
                   (lambda (a b)
                     (string-greaterp a b)))))
  (setf (ivy-state-preselect ivy-last) ivy--current)
  (ivy--reset-state ivy-last))

Now, we make a keymap to bind these commands so they are convenient to use. I will use C-arrows for swapping, and M-arrows for sorting the whole list. I also add M-<return> which allows me to use a numeric prefix to apply an action to all the candidates. M-<return> applies the default action. M-1 M-<return> applies the first action, M-2 M-<return> the second action, etc…

This specific implementation assumes your candidates have a cdr.

(setq ivy-sort-keymap
      (let ((map (make-sparse-keymap)))
        (define-key map (kbd "C-<up>") 'ivy-move-up)
        (define-key map (kbd "C-<down>") 'ivy-move-down)

        ;; sort all keys
        (define-key map (kbd "M-<up>") 'ivy-a-z)
        (define-key map (kbd "M-<down>") 'ivy-z-a)

        ;; map over all all entries with nth action
        (define-key map (kbd "M-<return>")
          (lambda (arg)
            "Apply the numeric prefix ARGth action to every candidate."
            (interactive "P")
            ;; with no arg use default action
            (unless arg (setq arg (car (ivy-state-action ivy-last))))
            (ivy-beginning-of-buffer)
            (let ((func (elt (elt (ivy-state-action ivy-last) arg) 1)))
              (loop for i from 0 to (- ivy--length 1)
                    do 
                    (funcall func
                             (let ((cand (elt
                                          (ivy-state-collection ivy-last)
                                          ivy--index)))
                               (if (listp cand)
                                   (cdr cand)
                                 cand)))
                    (ivy-next-line)))
            (ivy-exit-with-action
             (lambda (x) nil))))
        map))

Ok, now we modify our ivy-read function to use the keymap.

(defun ctn ()
  (interactive)
  (ivy-read "contact: " '(("Kno Body" "kb@true.you" "555-1212")
                          ("A. Person" "ap@some.come" "867-5304")
                          ("G. Willikers" "gw@not.me" "555-5555"))
            :keymap ivy-sort-keymap
            :action '(1
                      ("o" (lambda (x)
                             (with-ivy-window
                               (insert
                                (if (not (looking-back " ")) ", " "")
                                (elt x 0))))
                       "insert email")
                      ("p" (lambda (x)
                             (with-ivy-window
                               (insert
                                (if (not (looking-back " ")) ", " "")
                                (elt x 1))))
                       "insert phone"))))

kb@true.you, gw@not.me, ap@some.come, 555-1212, 555-5555, 867-5304

Copyright (C) 2016 by John Kitchin. See the License for information about copying.

org-mode source

Org-mode version = 8.3.4

Discuss on Twitter