Entries About Feed

Emacs custom helm actions

When I switched from neovim back to Emacs a year or two ago, something that my muscle-memory missed were the key bindings I had for denite.nvim for opening files & buffers in splits – C-s to open the result in a horizontal split and C-v for a vertical split. I like helm quite a bit as the equivalent of denite in Emacs and it has something like that by default, but it uses find-file-other-window, which I don’t really like; I find it confusing & somewhat unclear where the new window will actually be and I want complete control over which windows are appearing where.

It took a fair amount of fiddling & reading the source to figure out how to add custom actions to helm, so I thought I’d share what I figured out. My helm setup stuff can be found in context here, but I use some weird macro stuff to try to be somewhat lazy, so below is the general idea:

First, we define functions that will open the helm candidates in splits the way we want. Here, for example, is the function to open files in a horizontal split:

(defun helm-file-switch-to-new--horiz-window (_candidate)
  ;; The candidate to open is passed in to the function
  ;; but we're going to use `helm-marked-candidates' to explicitly
  ;; get the selected list of candidates anyway
  ;; (so the user can use C-SPC to select multiple files & open them all in
  ;; horizontal splits)
  (dolist (cand (helm-marked-candidates))
    ;; focus a new horizontal split...
    (select-window (split-window-right))
    ;; and open the file there
    (find-file cand))
  ;; adjust the windows after opening all those splits
  (balance-windows))

We then repeat for vertical splits, replacing split-window-right with split-window-below to define helm-file-switch-to-new--vert-window, then repeat both of those steps with find-file replaced by switch-to-buffer to make helm-buffer-switch-to--{horiz,vert}-window.

This is obviously a lot of boilerplate, which is why in my actual config linked above, I use cl-macrolet to define little local macros to generate these repeated function definitions.

Now that we have the functions defined, we can wire them up. However, it’s not quite as simple as just defining a key binding to call our new functions in the appropriate helm map. Instead, we need to wrap them in an additional function with some helm boilerplate, like so:

(defun helm-file-switch-new-horiz-window ()
  (interactive)
  (with-helm-alive-p
    (helm-exit-and-execute-action #'helm-file-switch-to-new--horiz-window)))
;; and the same for the other functions

Now, we can finally set up some key bindings to call our functions (I use general.el, but the equivalent with just define-key is straightforward):

(general-def helm-buffer-map
  "C-v" #'helm-buffer-switch-new-vert-window
  "C-s" #'helm-buffer-switch-new-horiz-window)
(general-def helm-projectile-find-file-map
  "C-v" #'helm-file-switch-new-vert-window
  "C-s" #'helm-file-switch-new-horiz-window)
(general-def helm-find-files-map
  "C-v" #'helm-file-switch-new-vert-window
  "C-s" #'helm-file-switch-new-horiz-window)

Now you can enjoy quickly & easily opening up buffers from your queries with complete control of how the new windows are being created!

Update 2019-01-19:

I cleaned up some of the above setup to also make the actions show up in helm’s list of actions when you hit tab, as well as giving them slightly nicer names (also added helm-rg bindings). The config now looks like this:

(use-package helm
  :config
  ;; ... some other stuff
  ;; Enable opening helm results in splits
  (cl-macrolet
      ((make-splitter-fn (name open-fn split-fn)
                         `(defun ,name (_candidate)
                            ;; Display buffers in new windows
                            (dolist (cand (helm-marked-candidates))
                              (select-window (,split-fn))
                              (,open-fn cand))
                            ;; Adjust size of windows
                            (balance-windows)))
       (generate-helm-splitter-funcs
        (op-type open-fn)
        (let* ((prefix (s-concat "helm-" op-type "-switch-"))
               (vert-split (intern (s-concat prefix "vert-window")))
               (horiz-split (intern (s-concat prefix "horiz-window"))))
          `(progn
             (make-splitter-fn ,vert-split ,open-fn split-window-right)

             (make-splitter-fn ,horiz-split ,open-fn split-window-below)

             (defun ,(intern (s-concat "helm-" op-type "-switch-vert-window-command"))
                 ()
               (interactive)
               (with-helm-alive-p
                 (helm-exit-and-execute-action (quote ,vert-split))))

             (defun ,(intern (s-concat "helm-" op-type "-switch-horiz-window-command"))
                 ()
               (interactive)
               (with-helm-alive-p
                 (helm-exit-and-execute-action (quote ,horiz-split))))))))
    (generate-helm-splitter-funcs "buffer" switch-to-buffer)
    (generate-helm-splitter-funcs "file" find-file)

    ;; install the actions for helm-find-files after that source is
    ;; inited, which fortunately has a hook
    (add-hook
     'helm-find-files-after-init-hook
     (lambda ()
       (helm-add-action-to-source "Display file(s) in new vertical split(s) `C-v'"
                                  #'helm-file-switch-vert-window
                                  helm-source-find-files)
       (helm-add-action-to-source "Display file(s) in new horizontal split(s) `C-s'"
                                  #'helm-file-switch-horiz-window
                                  helm-source-find-files)))

    ;; ditto for helm-projectile; that defines the source when loaded, so we can
    ;; just eval-after-load
    (with-eval-after-load "helm-projectile"
      (helm-add-action-to-source "Display file(s) in new vertical split(s) `C-v'"
                                 #'helm-file-switch-vert-window
                                 helm-source-projectile-files-list)
      (helm-add-action-to-source "Display file(s) in new horizontal split(s) `C-s'"
                                 #'helm-file-switch-horiz-window
                                 helm-source-projectile-files-list))

    ;; ...but helm-buffers defines the source by calling an init function, but doesn't
    ;; have a hook, so we use advice to add the actions after that init function
    ;; is called
    (defun cogent/add-helm-buffer-actions (&rest _args)
      (helm-add-action-to-source "Display buffer(s) in new vertical split(s) `C-v'"
                                 #'helm-buffer-switch-vert-window
                                 helm-source-buffers-list)
      (helm-add-action-to-source "Display buffer(s) in new horizontal split(s) `C-s'"
                                 #'helm-buffer-switch-horiz-window
                                 helm-source-buffers-list))
    (advice-add 'helm-buffers-list--init :after #'cogent/add-helm-buffer-actions))
  :general
  (:keymaps 'helm-buffer-map
   "C-v" #'helm-buffer-switch-vert-window-command
   "C-s" #'helm-buffer-switch-horiz-window-command)
  (:keymaps 'helm-projectile-find-file-map
   "C-v" #'helm-file-switch-vert-window-command
   "C-s" #'helm-file-switch-horiz-window-command)
  (:keymaps 'helm-find-files-map
   "C-v" #'helm-file-switch-vert-window-command
   "C-s" #'helm-file-switch-horiz-window-command))

(use-package helm-rg
  :config
  (defun cogent/switch-to-buffer-split-vert (name)
    (select-window (split-window-right))
    (switch-to-buffer name))
  (defun cogent/switch-to-buffer-split-horiz (name)
    (select-window (split-window-below))
    (switch-to-buffer name))

  (defun cogent/helm-rg-switch-vert (parsed-output &optional highlight-matches)
    (let ((helm-rg-display-buffer-normal-method #'cogent/switch-to-buffer-split-vert))
      (helm-rg--async-action parsed-output highlight-matches)))
  (defun cogent/helm-rg-switch-horiz (parsed-output &optional highlight-matches)
    (let ((helm-rg-display-buffer-normal-method #'cogent/switch-to-buffer-split-horiz))
      (helm-rg--async-action parsed-output highlight-matches)))

  ;; helm-rg defines the source when it's loaded, so we can add the action
  ;; right away
  (helm-add-action-to-source
   "Open in horizontal split `C-s'" #'cogent/helm-rg-switch-horiz
   helm-rg-process-source)
  (helm-add-action-to-source
   "Open in vertical split `C-v'" #'cogent/helm-rg-switch-vert
   helm-rg-process-source)

  (defun cogent/helm-rg-switch-vert-command ()
    (interactive)
    (with-helm-alive-p
      (helm-exit-and-execute-action #'cogent/helm-rg-switch-vert)))
  (defun cogent/helm-rg-switch-horiz-command ()
    (interactive)
    (with-helm-alive-p
      (helm-exit-and-execute-action #'cogent/helm-rg-switch-horiz)))

  (general-def helm-rg-map
    "C-s" #'cogent/helm-rg-switch-horiz-command
    "C-v" #'cogent/helm-rg-switch-vert-command))

The key things I realized this time is that the action (the function passed to helm-add-action-to-source) should just do the split and takes the candidate as the argument, while the key binding command should be interactive and call helm-exit-and-execute-action.