Extending project.el

In my first appreciation of project.el I wrote about a patch for project--files-in-directory. It’s a working solution, I won’t deny that, but patching code always feels hacky. It’s like a dirty workaround you cannot avoid to look at every time you visit your Emacs configuration.

By inspecting the code of project.el I noticed that project-files is a generic function. In Emacs Lisp parlance, a generic function specifies an abstract operation with the actual implementation provided by methods1. This simply means that I can devise my own implementation of project-files.

(cl-defmethod project-root ((project (head local)))
  (cdr project))

(defun mu--project-files-in-directory (dir)
  "Use `fd' to list files in DIR."
  (let* ((default-directory dir)
         (localdir (file-local-name (expand-file-name dir)))
         (command (format "fd -t f -0 . %s" localdir)))
     (sort (split-string (shell-command-to-string command) "\0" t)

(cl-defmethod project-files ((project (head local)) &optional dirs)
  "Override `project-files' to use `fd' in local projects."
  (mapcan #'mu--project-files-in-directory
          (or dirs (list (project-root project)))))

project.el has to be made aware of my local type now.

(defun mu-project-try-local (dir)
  "Determine if DIR is a non-Git project.
DIR must include a .project file to be considered a project."
  (let ((root (locate-dominating-file dir ".project")))
    (and root (cons 'local root))))

mu-project-try-local just needs to be added to project-find-functions to make sure my non-Git projects become known and remembered across sessions when I hit C-x p p. This is way more elegant than the previous patch.

Since I also never use Git submodules, I can push my extensions a little further.

(defun mu--backend (dir)
  "Check if DIR is under Git, otherwise return nil."
  (when (locate-dominating-file dir ".git")

(defun mu-project-try-vc (dir)
  "Determine if DIR is a project.
This is a thin variant of `project-try-vc':
- It takes only Git into consideration
- It does not check for submodules"
  (let* ((backend (mu--backend dir))
          (when (eq backend 'Git)
            (or (vc-file-getprop dir 'project-git-root)
                (let ((root (vc-call-backend backend 'root dir)))
                  (vc-file-setprop dir 'project-git-root root))))))
    (and root (cons 'vc root))))

mu-project-try-vc now replaces project-try-vc in project-find-functions.


  1. See Generic Functions in the manual.