Hot on the heels of Projectile 3.0 comes Projectile 3.1!

Three days apart, yes. There’s a story there. A big chunk of what’s in 3.1 was originally meant for 3.0, but 3.0 was already turning into a monster of a release and I decided to cut it into two, so I’d actually be able to reason about each of them. So think of 3.1 less as “the next release” and more as “the second half of 3.0 that I was too scared to ship all at once”.

There was also a bit of calendar mischief involved. I really wanted a release on the 1st of July - that’s July Morning, which is a bit of a thing here in Bulgaria1 - and then another one on the 4th of July. Partly because it’s the 250th US Independence Day, and partly because it happens to be my wedding anniversary. When else am I going to get a round number like that to line up? So here we are.

Unlike 3.0, 3.1 has no breaking changes. Nothing was removed, no command or option changed its name. What it does instead is knock out a pile of long-standing ideas and feature requests, most of which pull in the same direction: making Projectile leaner under the hood and a lot more extensible from your own config.

Projectile learns what you actually work on

projectile-find-file now ranks the files you visit most often and most recently at the top of the completion list. It’s the kind of thing you don’t notice until you go back to a Projectile without it and suddenly your muscle memory is off. It works with any completion UI (Vertico, the default one, whatever) and under every indexing method, and it’s on by default. If it’s not your thing, set projectile-enable-frecency to nil.

Named project tasks

The six lifecycle commands (compile, test, run, …) were never enough for real projects, which tend to accumulate a dozen little “run this incantation” commands. So there’s projectile-tasks now - an arbitrary set of named commands you can attach to a project. The nice part is you can put them in .dir-locals.el and share them with your team through the repo:

((nil . ((projectile-tasks . (("lint"   . "make lint")
                              ("deploy" . "make deploy STAGE=prod"))))))

Then s-p c x (projectile-run-task) prompts you for a task and runs it, with a prefix argument if you want to tweak the command first. This one subsumes about four separate feature requests I’d been staring at for years.

Finding files by kind

This is my favourite one. Lots of frameworks organize files into well-known kinds - Rails has models, controllers, views and helpers; Django has models, views and urls; and so on. Projectile can now describe those kinds declaratively, and it gives you two commands for free: s-p j to jump to a file of a particular kind, and s-p J to hop between related files (from a model to its controller to its views and back).

The best part is that it’s not hardcoded. Rails and Django ship out of the box, but the whole thing is driven by a :file-kinds entry on the project type, so you can teach Projectile about your own framework’s layout in a few lines. This is the “more extensible” theme in a nutshell - I’d much rather ship a mechanism than a hardcoded list.

Running the test at point

If you’re on Emacs 29+ with tree-sitter, s-p c . runs just the test your cursor is in, rather than the whole suite. It figures out the enclosing test from the parse tree and builds the right command. pytest, go test and jest are supported out of the box, and - you guessed it - you can register rules for other test runners yourself.

Other-window and other-frame, the Emacs way

Instead of a small army of -other-window/-other-frame command variants, there are now s-p 4 4 and s-p 5 5 prefixes, modeled exactly on Emacs’s own C-x 4 4 / C-x 5 5. Press s-p 4 4 and the next Projectile command opens its buffer in another window - even commands that never had a dedicated variant. The old commands still work, of course.

More solid under the hood

A few things got quietly sturdier:

  • File-notify cache updates. projectile-invalidate-cache has always been Projectile’s most notorious footgun. If you opt into projectile-auto-update-cache-with-watches, Projectile watches your project and keeps its file cache in sync as files come and go, so you rarely have to think about invalidation at all. It’s off by default and deliberately conservative - when in doubt it just rebuilds.
  • .projectile finally makes sense. The glob patterns in your dirconfig now follow .gitignore-style rules, and they behave identically under native and hybrid indexing. This kills a whole family of “why is this file showing up” bug reports that go back the better part of a decade.
  • TRAMP got faster again. Project and project-type detection now probe marker files with a single directory listing instead of a stat per marker, which turns dozens of sequential round-trips into one over TRAMP.

Wrapping up

There are no breaking changes, but a handful of defaults and behaviors did shift (auto-discovery is on by default now, for one), so give the Upgrading to Projectile 3.1 guide a quick read before you upgrade. The full list of changes lives in the changelog, as always.

With 3.1 out the door, Projectile is basically where I always wanted it to be. I’ve got a few ideas for follow-up releases, but the pressure is off - this is the release where the big items I’d been carrying around for years finally got done. Another win for my burst-driven approach to maintaining my projects: nothing happens for a while, and then a whole lot happens at once.

Keep hacking!

  1. July Morning is a Bulgarian tradition where people head to the Black Sea coast to greet the sunrise on the 1st of July. It started as a hippie / rock-and-roll thing in the 80s and stuck around. Highly recommended, if you ever get the chance.