(or emacs irrelevant)

A Hydra for ivy/swiper

Today I'll share a Hydra that I've been working on that's similar to Lit Wakefield's original idea for a helm hydra.

The Code

(defhydra hydra-ivy (:hint nil
                     :color pink)
  "
^^^^^^          ^Actions^    ^Dired^     ^Quit^
^^^^^^--------------------------------------------
^ ^ _k_ ^ ^     _._ repeat   _m_ark      _i_: cancel
_h_ ^✜^ _l_     _r_eplace    _,_ unmark  _o_: quit
^ ^ _j_ ^ ^     _u_ndo  
"
  ;; arrows
  ("h" ivy-beginning-of-buffer)
  ("j" ivy-next-line)
  ("k" ivy-previous-line)
  ("l" ivy-end-of-buffer)
  ;; actions
  ("." hydra-repeat)
  ("r" ivy-replace)
  ("u" ivy-undo)
  ;; dired
  ("m" ivy-dired-mark)
  ("," ivy-dired-unmark)
  ;; exit
  ("o" keyboard-escape-quit :exit t)
  ("i" nil))

Here's how I bind it:

(define-key ivy-minibuffer-map (kbd "C-o") 'hydra-ivy/body)

And here are the auxiliaries:

(defun ivy-dired-mark (arg)
  (interactive "p")
  (dotimes (_i arg)
    (with-ivy-window
      (dired-mark 1))
    (ivy-next-line 1)
    (ivy--exhibit)))

(defun ivy-dired-unmark (arg)
  (interactive "p")
  (dotimes (_i arg)
    (with-ivy-window
      (dired-unmark 1))
    (ivy-next-line 1)
    (ivy--exhibit)))

(defun ivy-replace ()
  (interactive)
  (let ((from (with-ivy-window
                (move-beginning-of-line nil)
                (when (re-search-forward
                       (ivy--regex ivy-text) (line-end-position) t)
                  (match-string 0)))))
    (if (null from)
        (user-error "No match")
      (let ((rep (read-string (format "Replace [%s] with: " from))))
        (with-selected-window swiper--window
          (undo-boundary)
          (replace-match rep t t))))))

(defun ivy-undo ()
  (interactive)
  (with-ivy-window
    (undo)))

The dired operations

There's actually an outstanding issue to make the hydra heads appear conditionally. This would be quite useful for the m and , bindings, since they don't work outside a dired buffer. Maybe I'll get to it on the weekend. Meanwhile, here's a screenshot for marking files in dired using swiper:

hydra-ivy-dired.png

Since the input is "mar 10", swiper transforms it into the regex "(mar).*(10)". What I do next:

  • C-o to get into the hydra state.
  • 99m to mark everything. Normally, it would mark 99 candidates, but since there are only 17, that means all of them.
  • h to go to the first candidate.
  • j, to to skip one candidate and unmark, then unmark some more, using this method.

If I wanted to move two candidates down at once, I could press 2j...... The . will repeat the previous command with the previous argument. You can also set the argument later, e.g. j.2.3.. etc.

The exit points

There are two:

  • i will bring you back to ivy, so that you can edit the input.
  • o will quit everything and bring you to the dired buffer.

So you could first mark Mar 10, exit with i, edit the input to Mar 17, press C-o and mark some more. Then finally exit with o.

The replace and undo operations

These two I've added the latest, so they are still a bit off. The ivy-replace option is similar to vim's r (I did vimtutor yesterday). It lets you replace the selected candidate. And u simply calls undo. Strangely, at the moment it will undo several ivy-replace operations at once, even though I call undo-boundary in ivy-replace.

Outro

I think ivy and the hydra docstring blend in together quite nicely, like old dogs and new tricks. I don't know which is which.