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