One of the central pieces of my recent return to
Helm is
helm-ls-git, which empowers
helm-browse-project
with everything I need to move through my Git projects
smoothly. Combine that with helm-projects-history
, and you can see why I don’t
need Projectile any more. Well, sort of. It’s true that I’ve stopped using
Projectile since Helm took over my Emacs configuration, but it’s also true that
I had to code the “helmified” version of a couple of features I was heavily
relying upon.
In Helm, you can leverage actions to extend the available utilities.
(defun mu--candidate-directory (candidate)
"Find the directory containing CANDIDATE.
Default to `helm-current-directory' when none can be found."
(if (fboundp 'helm-ls-git-root-dir)
(let* ((dir (cond ((stringp candidate)
(file-name-directory candidate))
((and (bufferp candidate)
(buffer-file-name candidate))
(file-name-directory (buffer-file-name candidate)))
(t default-directory)))
(root-dir (helm-ls-git-root-dir dir)))
(if root-dir
root-dir
(helm-current-directory)))
(helm-current-directory)))
(defun mu-helm-project-dired ()
"Open Dired buffers at project root from a Helm session."
(interactive)
(dolist (cand (helm-marked-candidates))
(dired (mu--candidate-directory cand))))
(defun mu-helm-project-shell ()
"Open shell buffers at project root from a Helm session."
(interactive)
(dolist (cand (helm-marked-candidates))
(let* ((default-directory (mu--candidate-directory cand))
(buffer (concat "*shell "
(file-name-nondirectory
(directory-file-name default-directory))
"*")))
(mu-shell-open buffer))))
The actions are self-explanatory (see Passing the prefix argument
around for mu-shell-open
).
I often need to jump to a Dired or a shell-mode buffer of a project different
from the current one, so I want the possibility to use the current candidate in
helm-projects-history
as a starting point.
To be fair, mu--candidate-directory
does a little more than what I need here.
That’s because I use mu-helm-project-dired
and mu-helm-project-shell
in
helm-mini
and helm-browse-project
too. The latter lists special buffers
(e.g., *scratch*
) as buffers of the current project. Since they do not live in
any specific directory,
default-directory
is the obvious choice.
Now I have to plug the actions into helm-projects-history
. Unfortunately, it
doesn’t have a keymap
to extend, so I need another solution.
(defvar mu-helm-projects-history-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map helm-map)
(define-key map (kbd "C-c d") (helm-exit-and-run! (mu-helm-project-dired)))
(define-key map (kbd "C-c s") (helm-exit-and-run! (mu-helm-project-shell)))
map)
"Keymap for `mu-helm-projects-history'.")
(defun mu-helm-projects-history (arg)
"A `helm-projects-history' with custom actions."
(interactive "P")
(require 'helm-ls-git)
(helm :sources
(helm-build-sync-source "Project history"
:candidates helm-browse-project-history
:action (lambda (candidate)
(with-helm-default-directory candidate
(helm-browse-project
(or arg helm-current-prefix-arg))))
:keymap mu-helm-projects-history-map)
:buffer "*helm browse project history*"))
mu-helm-projects-history
adds only two things to the original
helm-projects-history
. I require helm-ls-git
early because I need its
capabilities inside my actions, and I use my own mu-helm-projects-history-map
to make them available in the Helm buffer opened by this command.
Last but not least, helm-exit-and-run!
is a helpful macro to avoid dry
repetitions:
(defmacro helm-exit-and-run! (&rest body)
"Define an action with BODY to be run after exiting Helm."
(declare (doc-string 1))
`(lambda ()
(interactive)
(with-helm-alive-p
(helm-exit-and-execute-action (lambda (_candidate) ,@body)))))
Note that in my actions I am ignoring the _candidate
parameter on purpose.
Since helm-marked-candidates
grabs the current candidate when none of them is
selected, I can use my actions on multiple candidates if I feel wild enough.
Yeah, sometimes I can be crazy like that.