Posts tagged "project-el":

18 Feb 2026

Switching to project.el

I've used projectile ever since I created my own Emacs config. I have a vague memory choosing it because some other package only supported it. (It might have been lsp-mode, but I'm not sure.) Anyway, now that I'm trying out eglot, again, I thought I might as well see if I can switch to project.el, which is included in Emacs nowadays.

A non-VC project marker

Projectile allows using a file, .projectile, in the root of a project. This makes it possible to turn a folder into a project without having to use version control. It's possible to configure project.el to respect more VC markers than what's built-in. This can be used to define a non-VC marker.

(setopt project-vc-extra-root-markers '(".projectile" ".git"))

Since I've set vc-handled-backends to nil (the default made VC interfere with magit, so I turned it off completely) I had to add ".git" to make git repos be recognised as projects too.

Xref history

The first thing to solve was that the xref stack wasn't per project. Somewhat disappointingly there only seems to be two options for xref-history-storage shipped with Emacs

xref-global-history
a single global history (the default)
xref-window-local-history
a history per window

I had the same issue with projectile, and ended up writing my own package for it. For project.el I settled on using xref-project-history.

(use-package xref-project-history
  :ensure (:type git
           :repo "https://codeberg.org/imarko/xref-project-history.git"
           :branch "master")
  :custom
  (xref-history-storage #'xref-project-history))

Jumping between implementation and test

Projectile has a function for jumping between implementation and test. Not too surprisingly it's called projectile-toggle-between-implementation-and-test. I found some old emails in an archive suggesting that project.el might have had something similar in the past, but if that's the case it's been removed by now. When searching for a package I came across this email comparing tools for finding related files. The author mentions two that are included with Emacs

ff-find-other-file
part of find-file.el, which a few other functions and a rather impressive set of settings to customise its behaviour.
find-sibling-file
a newer command, I believe, that also can be customised.

So, there are options, but neither of them are made to work nicely with project.el out of the box. My most complicated use case seems to be in Haskell projects where modules for implementation and test live in separate (mirrored) folder hierarchies, e.g.

src
└── Sider
    └── Data
        ├── Command.hs
        ├── Pipeline.hs
        └── Resp.hs
test
└── Sider
    └── Data
        ├── CommandSpec.hs
        ├── PipelineSpec.hs
        └── RespSpec.hs

I'm not really sure how I'd configure find-sibling-rules, which are regular expressions, to deal with folder hierarchies like this. To be honest, I didn't really see a way of configuring ff-find-other-file at first either. Then I happened on a post about switching between a module and its tests in Python. With its help I came up with the following

(defun mes/setup-hs-ff ()
  (when-let* ((proj-root (project-root (project-current)))
              (rel-proj-root (-some--> (buffer-file-name)
                               (file-name-directory it)
                               (f-relative proj-root it)))
              (sub-tree (car (f-split (f-relative (buffer-file-name) proj-root))))
              (search-dirs (--> '("src" "test")
                                (remove sub-tree it)
                                (-map (lambda (p) (f-join proj-root p)) it)
                                (-select #'f-directory? it)
                                (-mapcat (lambda (p) (f-directories p nil t)) it)
                                (-map (lambda (p) (f-relative p proj-root)) it)
                                (-map (lambda (p) (f-join rel-proj-root p)) it))))
    (setq-local ff-search-directories search-dirs
                ff-other-file-alist '(("Spec\\.hs$" (".hs"))
                                      ("\\.hs$" ("Spec.hs"))))))

A few things to note

  1. The order of rules in ff-other-file-alist is important, the first match is chosen.
  2. (buffer-file-name) can, and really does, return nil at times, and file-name-directory doesn't deal with anything but strings.
  3. The entries in ff-search-directories have to be relative to the file in the current buffer, hence the rather involved varlist in the when-let* expression.

With this in place I get the following values for ff-search-directories

src/Sider/Data/Command.hs
("../../../test/Sider" "../../../test/Sider/Data")
test/Sider/Data/CommandSpec.hs
("../../../src/Sider" "../../../src/Sider/Data")

And ff-find-other-file works beautifully.

Conclusion

My setup with project.el now covers everything I used from projectile so I'm fairly confident I'll be happy keeping it.

Tags: emacs project-el
Other posts