Firestarter with project root directories

firestarter is an emacs package used for running arbitrary commands upon saving a file. Out of the box, it comes with support for running shell commands, elisp functions and arbitrary emacs lisp code. The most interesting and likely most common use case is to run shell commands. firestarter provides an interesting format specification for shell commands, e.g., %f is replaced by the name of the file backing the buffer, %p by the path to the file, etc.

As someone working with embedded devices for most of which are connected to some remote machine in some lab, and as one who prefers to edit code within emacs from files on a local filesystem (see below for reasons), I often run into the issue of copying local edits to a remote machine during normal edit-compile-run cycles. So my ideal set up would have been to use firestarter to copy the files over every time any files in such a project were saved from emacs. A directory local variable with contents along the lines of

(firestarter . "scp %p remotemachine:/path/to/project/%r")

in the root directory of the project would have been great, if %r could represent the path of the file relative to the project root. Unfortunately, firestarter does not provide such a format specifier. However, since this is emacs, it is easy to whip up a function to do what we want:

(defun ravi/find-path-name-relative-to-project-root ()
  "Find path of current buffer file relative to project root"
  (let* ((root-dir (and (buffer-file-name)
                        (or (and (fboundp 'magit-get-top-dir) (magit-get-top-dir))
                            (and (fboundp 'vc-root-dir) (vc-root-dir))
                            (locate-dominating-file (buffer-file-name) ".git")
                            (locate-dominating-file (buffer-file-name) dir-locals-file))))
         (relative-name (and root-dir
                             (file-relative-name (buffer-file-name) root-dir))))

(defun ravi/apply-shell-command-relative-to-project (command-template)
  "Apply shell command relative to project

COMMAND-TEMPLATE should be a string with the following substitutions allowed:

%p: full path to file name

%r: path relative to project root

%f: file name

%N: unquoted relative path name (should be used extremely carefully)
  (interactive "sEnter command template: ")
  (let* ((path (shell-quote-argument (or (buffer-file-name) "")))
         (rel-name (ravi/find-path-name-relative-to-project-root))
         (rel-path (shell-quote-argument (or rel-name "")))
         (file (shell-quote-argument (file-name-nondirectory (or path ""))))
         (cmd (and rel-name
                   (format-spec command-template
                                (format-spec-make ?p path ?r rel-path ?f file ?N rel-name)))))
    (if cmd
        (shell-command cmd)
      (if (not (buffer-file-name))
          (message "Not in a file buffer")
        (message "Unable to find project root")))))

Writing error messages and docstrings was actually harder than writing the code itself. There are two interesting bits:

  • try magit-get-top-dir before other methods to get the root directory since it works with files in the project yet to be added to version control
  • use dir-locals-file (normally .dir-locals.el) to set project root in case everything else fails

It is trivial to extend this to many other version control systems. With the above, we can use firestarter to copy files:

(firestarter . (ravi/apply-shell-command-relative-to-project
                "scp %p remotemachine:/path/to/project/%r"))

(The code can be found in my .emacs repository.)

Of course, one could say that remote editing with emacs tramp accomplishes the same goal by editing remote files. However, for large projects, essential tools such as helm-ls-git, git-messenger, and helm-ag are far more responsive and usable with local filesystems. A lot of the remote systems I use have ancient versions of tools which are painful to even contemplate using on a regular basis.


Comments powered by Disqus