Over the years, I have tried many different "project management" libraries for emacs -- enough that I do not even remember which ones. I do not really remember finding one that did what I wanted (or could be coaxed into it within any reasonable amount of time). Sometime last year I stitched together something rudimentary that serves my workflow very well. With my recent emacs configuration rewrite, I took the opportunity to refine my solution into something slightly nicer. I am documenting it here partly so I do not forget what I did, but it may be useful to someone else.
Requirements
First, let me outline my simple requirements for project management:
-
I need to be able to build tags (and have them automatically loaded) for a project.
-
I need to be able to easily build a project.
Note that my projects do not necessarily have a one-to-one
correspondence with source code repositories. I have a few large
projects split over multiple repositories that I would prefer to treat
as a single projects. In particular, I want a single TAGS
file that
has tags spanning multiple repositories for easy source code
navigation.
Solution
I ended up using simple
directory local variables.
This emacs feature lets you put a file named .dir-locals.el
at the
root of a source tree. When a file is loaded, the first
.dir-locals.el
above it in the file system is loaded. Variables
defined in .dir-locals.el
are defined as buffer locals in the opened
file. I define two directory local variables for each project:
tag-builder
and build-method
. I use the following elisp to build
my project management functionality:
;;; Tag support
(defun make-tags-hook ()
(when (boundp 'tag-builder)
(let ((project-root (locate-dominating-file buffer-file-name ".dir-locals.el")))
(let ((cmd (format "cd %s ; %s &" project-root tag-builder)))
(call-process "bash" nil 0 nil "-c" cmd)))))
(add-hook 'after-save-hook 'make-tags-hook)
(defun set-project-tags ()
(let ((project-root (locate-dominating-file buffer-file-name ".dir-locals.el")))
(let ((tags-file (format "%s/TAGS" project-root)))
(when (file-readable-p tags-file)
(visit-tags-table tags-file)))))
(add-hook 'find-file-hook 'set-project-tags)
;;; Compile support
(defun set-compile-command ()
"Set the compile command based on a `.dir-locals.el' build-method"
(when (boundp 'build-method)
(let ((project-root (locate-dominating-file buffer-file-name ".dir-locals.el")))
(setq compile-command (format "cd %s ; %s" project-root build-method)))))
(add-hook 'find-file-hook 'set-compile-command)
The make-tags-hook
function rebuilds the tags for an entire project
and is run after each file is saved. In practice this has been fast
enough for me. I have only tried it on ~20,000 lines of Haskell code,
though. Simply, it finds the closest .dir-locals.el
file and
assumes that is the project root. It changes to that directory and
runs the tag building command from the directory local variable file.
The next function, set-project-tags
selects the appropriate TAGS
file every time a new file is opened (using visit-tags-table
). This
is not exactly ideal. Only one TAGS
file can be loaded at a time,
normally, so working with multiple projects in one emacs session
would not work perfectly. Luckily, I do not do that very often.
This shortcoming could be fixed with something like
etags-table. I might
try to do that soon.
The last function deals with compilation support. It uses the
build-method
directory local to define an appropriate
compile-command
for the project. The key functionality here is that
the computed compile-command
changes directory to the root of the
project before running the build command. Again, the project root is
determined based on the location of .dir-locals.el
. Since this just
modifies compile-command
, the normal build infrastructure (like M-x compile
) Just Works.
Notes
I used unconventional names for my directory local variables. I
wanted to use build-command
and tag-builder-command
, but there is
an interesting quirk in directory local variables. For security
purposes, only variables explicitly marked "safe" in emacs are loaded
without user interaction. The user is interactively prompted before
so-called "risky" variables are loaded. These "risky" variables can
be trusted (and recorded in custom.el
) easily. Variables that are
fundamentally unsafe, or ending in "-command" (or a few other
suffixes) can be loaded explicitly by the user, but cannot be marked
as safe in custom.el
. This was very annoying, so I just chose some
unconventional names to work around it. This is in some sense
"unsafe", but you are still prompted to accept new values of these
variables the first time they are encountered. This is a fine
tradeoff for simplicity and convenience, for me.