Skip to content

Dynamic sorting in ivy

John Kitchin edited this page Jun 13, 2016 · 1 revision

See http://kitchingroup.cheme.cmu.edu/blog/2016/06/13/Dynamic-sorting-with-ivy/ for the original post.

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"))))