(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.

Swiper 0.2.0 is out

I forgot to mark the 0.1.0 release, so I'm giving an overview of all the fixes and new features since the first commit in the release notes.

Fixes

Fix font locking in certain modes

Some major modes try to optimize the font locking (highlighting text with various faces) by only doing it for the visible portion of the text. But since swiper needs to access all lines at once, it's necessary to font lock the whole buffer. This is done in swiper-font-lock-ensure. For some modes, the buffer becomes discolored after calling swiper-font-lock-ensure. In theory, this should not happen. As a work-around, I exclude these modes from ensuring font lock:

  • package-menu-mode
  • gnus-summary-mode
  • gnus-article-mode
  • gnus-group-mode
  • emms-playlist-mode
  • erc-mode

If you see a discoloration in one of your favorite major modes while using swiper, just let me know and I'll add it to the list.

Fix face changes in the minibuffer propagating to the main buffer

This was a quite interesting bug. At that moment, I was using add-face-text-property to add faces to the copies of strings in the minibuffer. However, this function destructively modifies the properties, so the change to the properties of a string copy (obtained with concat or copy-sequence) was propagated to the properties of the original string. This was fixed by using font-lock-append-text-property instead of add-face-text-property.

ivy-read returns immediately for 0-1 candidates

An obvious improvement.

Clean up overlays better for C-g

The hidden overlays revealed during the search will be re-hidden if you cancel the search with C-g.

Ensure that candidates don't have read-only property

This issue was causing a bug while using swiper in erc-mode, since it marks all of the buffer content to read-only. So once a string with read-only property is inserted into the minibuffer, you can't delete it unless you set inhibit-read-only.

New Features

Optional initial input

If you call (swiper "fix"), you'll start searching with initial input "fix".

Restore the initial point on canceling

If you cancel the search with e.g. C-g (or DEL when there's no input), the initial point will be restored.

Inherit standard faces

To give a more standard default appearance, swiper faces inherit the default faces:

  • isearch-lazy-highlight-face
  • isearch
  • match
  • isearch again
  • highlight

Most themes customize these faces, so by re-using them swiper blends in better. If you want the cool (my opinion) original faces, have a look at eclipse-theme.

Reveal invisible overlays

This is quite important for searches in org-mode buffers. I tried to make it as close as possible to what isearch is doing.

Mark is saved for successful searches

This is the behavior of isearch. After you complete a search, you can go back to the search start with C-x C-SPC (pop-global-mark).

The current candidate is anchored to the current position

This means that if many candidates are matching the current input, the one which is closest to the current line (going forwards) is selected. This is important for not losing the context of what you're searching.

Don't recenter unless necessary

This is similar to the behavior of isearch: a scroll is performed only if the candidate is out of the window bounds. An alternate strategy of keeping the current candidate always centered in the window is more distracting.

Decouple helm back end

swiper command uses only ivy now. If you want to use helm, have a look at swiper-helm.

Add history handling

M-n will select the next history element, and M-p will select the previous history element.

When there is no input, both C-s and C-r will select the last history element. This is to make it similar to isearch.

When there is no input, and only once during the search, M-n will select symbol-at-point as the current input.

Truncate candidates to window width in the minibuffer

If a candidate is longer than the window width, it will be appropriately truncated with .... You can still match the invisible parts.

Warn for empty buffer

Obviously there's nothing to search for in an empty buffer.

Bring the last history candidate to front

In case of a successful search, the current input will be removed from history, and then re-added to the front.

Make C-n and C-p differ from C-s and C-r

The arrows will not recall the last history element in case the input is empty. Otherwise, C-n matches to C-s and C-p matches to C-r.

ivy-read now displays the number of candidates in the prompt

The prompt argument can hold a format-style expression, e.g. " [% 3d] pattern:", and the integer parameter will be updated with the current amount of matches. You can also customize ivy-count-format, that defaults to "%-4d ".

Custom amount of chars to start highlighting

Customize swiper-min-highlight for this. It defaults to 2, which means that the current buffer will be highlighted when the input has 2 chars or more. You can set it to 1 if you want, I found it slightly distracting, since there will be a lot of highlights for just one char input. Or you can set it to a larger value if you want the highlights to appear later.

Customize wrapping for C-n and C-p

This feature being on by default in helm-swoop was very distracting for me, and one of the reasons that I wrote swiper. So it's off by default, but you can set it if you want. Calling C-p on line number 0 will cycle to the last candidate etc.

Since swiper was added to GNU ELPA, I had to assign the Copyright to the FSF. This also means that you also need an FSF Copyright assignment for Emacs in order to contribute more than total of 15 lines to the swiper code. It's really easy to get and is already necessary to contribute to any package that is part of Emacs.

Add swiper-query-replace

You can start a query replace operation starting with the current candidate with M-q. If you want to query replace in whole buffer, just do M-< M-q. And remember that ! will auto-replace all matches for the current query.

The default binding of M-q is fill-paragraph. This function is useless in the minibuffer, so I chose that binding for query-replace, which is normally bound to sub-optimal M-%. Although there are worse bindings than M-% (hold three keys at once), for instance, there's C-M-% (hold four keys at once) that calls query-replace-regexp.

Outro

Enjoy the new features, and a big thanks to all who contributed!

Transform region into ASCII art

Here's a command that I've found laying around my config that you might find interesting:

(defun ora-figlet-region (&optional b e)
  (interactive "r")
  (shell-command-on-region b e "toilet" (current-buffer) t))

You can install the toilet shell utility from your package manager. Here's an example result:

    #"
   m"    mmm    m mm          mmm   mmmmm   mmm    mmm    mmm
   #    #" "#   #"  "        #"  #  # # #  "   #  #"  "  #   "
   #    #   #   #            #""""  # # #  m"""#  #       """m
    #   "#m#"   #            "#mm"  # # #  "mm"#  "#mm"  "mmm"
     "

If you don't mind the strange name, the output looks quite nice.

Hydra 0.12.0 is out

With a month's time and almost 50 commits since that last one, a new version of Hydra has emerged. As usual, I'll just re-state the release notes.

Fixes

  • Handling of heads with duplicate cmd was improved.
  • Don't bind nil in outside keymaps.
  • Work-around golden-ratio-mode in lv-window.
  • C-g (hydra-keyboard-quit) should run :post.
  • Bind [switch-frame] to hydra-keyboard-quit.
  • :post is called for :timeout.

New Features

  • hydra-key-format-spec is a new defcustom for the keys format in the docstring. It's "%s" by default, but you can set it to e.g. "%-4s" if you like.
  • The key regex was extended to include most common key binding characters.
  • hydra-repeat is a hydra-specific repeat function. It behaves as you would expect repeat to behave.
  • New body option - :timeout. Use e.g. :timeout 2.0 to set the timer. After the first head is called, a timer is started to disable the hydra. Each new head call resets this timer, so the hydra won't disappear as long as you keep typing.
  • Lines are truncated in lv-message. This is useful for large docstring not to become misaligned when the window becomes too small.

Allow for a %s(test) spec in the docstring

The spec that's used for e.g. (test) is %S. So if (test) returns a string, it will be quoted. This may not be desired, hence the new feature. Example:

(defhydra hydra-marked-items (dired-mode-map "")
  "
Number of marked items: %(length (dired-get-marked-files))
Directory size: %s(shell-command-to-string \"du -hs\")
"
  ("m" dired-mark "mark"))

The pink/amaranth override is set recursively

This fixes the issue in this hydra:

(defhydra hydra-test (:color amaranth)
  "foo"
  ("fo" (message "yay"))
  ("q" nil))

Before, pressing e.g. fp would not issue a warning, since f started its own keymap. This is now fixed.

An option to specify the hint for all heads in body

When you write a large docstring, you usually pass nil as the hint for most heads. Now you can omit it, if you set :hint nil in body. Example:

(defhydra hydra-org-template (:color blue :hint nil)
  "
_c_enter  _q_uote    _L_aTeX:
_l_atex   _e_xample  _i_ndex:
_a_scii   _v_erse    _I_NCLUDE:
_s_rc     ^ ^        _H_TML:
_h_tml    ^ ^        _A_SCII:
"
  ("s" (hot-expand "<s"))
  ("e" (hot-expand "<e"))
  ("q" (hot-expand "<q"))
  ("v" (hot-expand "<v"))
  ("c" (hot-expand "<c"))
  ("l" (hot-expand "<l"))
  ("h" (hot-expand "<h"))
  ("a" (hot-expand "<a"))
  ("L" (hot-expand "<L"))
  ("i" (hot-expand "<i"))
  ("I" (hot-expand "<I"))
  ("H" (hot-expand "<H"))
  ("A" (hot-expand "<A"))
  ("<" self-insert-command "ins")
  ("o" nil "quit"))

Emulate org-mode export dispatch with hydra-ox

You can also look at that code to see how nested hydras work. Several other examples were added to hydra-examples.el.

Outro

I hope that you enjoy all the new features/fixes, and thanks to all the people that contributed to them. Happy hacking!

Some fun with Hydra

The code from this post has very little application. But it's kind of fun, so I'll post it. Star Trek: TNG is one of my favorite shows, so I've added some TNG characters to one of the Hydra features that I'm testing.

defhydradio statement

(require 'hydra)
(defhydradio hydra-tng ()
  (picard "_p_ Captain Jean Luc Picard:")
  (riker "_r_ Commander William Riker:")
  (data "_d_ Lieutenant Commander Data:")
  (worf "_w_ Worf:")
  (la-forge "_f_ Geordi La Forge:")
  (troi "_t_ Deanna Troi:")
  (dr-crusher "_c_ Doctor Beverly Crusher:")
  (phaser "_h_ Set phasers to " [stun kill]))

The defhydradio macro is akin to a namespace construct that defines multiple variables that can assume only certain values (either t or nil by default), and functions to cycle those values.

defhydradio implementation

Here's what you may see after a macroexpand:

(progn
  (defvar hydra-tng/picard nil
    "_p_ Captain Jean Luc Picard:")
  (put (quote hydra-tng/picard)
       (quote range)
       [nil t])
  (defun hydra-tng/picard nil
    (hydra--cycle-radio (quote hydra-tng/picard)))
  (defvar hydra-tng/riker nil
    "_r_ Commander William Riker:")
  (put (quote hydra-tng/riker)
       (quote range)
       [nil t])
  (defun hydra-tng/riker nil (hydra--cycle-radio (quote hydra-tng/riker)))
  ;; ...
  (defvar hydra-tng/names
    '(hydra-tng/picard hydra-tng/riker
      hydra-tng/data hydra-tng/worf hydra-tng/la-forge
      hydra-tng/troi hydra-tng/dr-crusher hydra-tng/phaser)))

As you can see, each list passed to defhydradio:

  • gets a prefixed variable definition
  • gets a range property for the prefixed symbol
  • gets a prefixed function definition that cycles the variable value based on the range property
  • gets added to hydra-tng/names

defhydra statement

(defhydra hydra-tng (:foreign-keys run :hint nil)
  (concat (hydra--table hydra-tng/names 7 2
                        '("  % -30s %% -3`%s"
                          "%s %%`%s"))
          "\n\n")
  ("p" (hydra-tng/picard))
  ("r" (hydra-tng/riker))
  ("d" (hydra-tng/data))
  ("w" (hydra-tng/worf))
  ("f" (hydra-tng/la-forge))
  ("t" (hydra-tng/troi))
  ("c" (hydra-tng/dr-crusher))
  ("h" (hydra-tng/phaser))
  ("b" beam-down "beam down" :exit t)
  ("o" (hydra-reset-radios hydra-tng/names) "reset")
  ("q" nil "cancel"))

The interesting statement in place of the docstring will actually evaluate to this docstring:

"  _p_ Captain Jean Luc Picard:   % -3`hydra-tng/picard^^^^    _h_ Set phasers to  %`hydra-tng/phaser
  _r_ Commander William Riker:   % -3`hydra-tng/riker^^^^^
  _d_ Lieutenant Commander Data: % -3`hydra-tng/data^^^^^^
  _w_ Worf:                      % -3`hydra-tng/worf^^^^^^
  _f_ Geordi La Forge:           % -3`hydra-tng/la-forge^^
  _t_ Deanna Troi:               % -3`hydra-tng/troi^^^^^^
  _c_ Doctor Beverly Crusher:    % -3`hydra-tng/dr-crusher

"

The first line overflows a bit, but it's clear what it is. There's some flexibility in using hydra--table, since you can:

  • redefine the row-column format (e.g. from 7x2 to 5x3)
  • add more variables to hydra-tng/names

Note also, that since hydra-tng/names holds all the names, and all the names know their default values through range, it's possible to reset them all at once with hydra-reset-radios.

Finally, here's a simple implementation of beam-down:

(defun beam-down ()
  (interactive)
  (message
   "Beaming down: %s."
   (mapconcat
    #'identity
    (delq nil
          (mapcar
           (lambda (p) (when (symbol-value p)
                    (substring (symbol-name p) 10)))
           '(hydra-tng/picard
             hydra-tng/riker
             hydra-tng/data
             hydra-tng/worf
             hydra-tng/la-forge
             hydra-tng/troi
             hydra-tng/dr-crusher)))
    ", and ")))

(global-set-key (kbd "C-c C-,") 'hydra-tng/body)

Outro

And that's it. There actually is an application of defhydradio in hydra-ox.el. It's not fully finished, but you can already try it as an alternative to org-mode export dispatch widget, most things are working.