Projectile 3.3
Projectile 3.3 is out! That’s the fourth release this month, which probably tells you something about how much fun I’ve been having with Projectile lately.
This one is mostly about a single theme - Projectile knowing more about your project without you having to tell it anything.
Tasks you never configured
Projectile has had named tasks for a while now - you put an alist in projectile-tasks
(or in your project’s .dir-locals.el) and projectile-run-task (s-p c x) offers
them with completion. Useful, but there was a catch that always bugged me: the variable
starts out empty, so the command did precisely nothing until you sat down and filled it in.
Meanwhile every project I work on already has its tasks written down somewhere. The npm
scripts are in package.json. The Makefile targets are in the Makefile. So now Projectile
just reads them:

That’s a fresh checkout with zero Projectile configuration. Out of the box it knows about
npm scripts (running them through whichever package manager your lock file implies - npm,
yarn, pnpm or bun), Deno tasks, Composer scripts, just recipes, go-task tasks and Makefile
targets. The tool name is part of the task name, so npm:build and make:build can happily
coexist and you can see at a glance where something came from.
If you don’t want this, projectile-discover-tasks turns it off. If you want more of it,
projectile-task-providers is just a list of functions that take a project root and return
an alist - adding support for your favorite task runner is a few lines.
A lot more project types
Projectile ships with a registry of project types, and I finally sat down and went through it properly. The result is 34 new types.
The one that surprised me most was that a package.json without a lock file next to it
matched nothing at all - such a project came out as generic, with no compile or test
command. That can’t have been a rare situation. Also missing: Bun, Deno, uv (which has
pretty much taken over new Python projects), plain Composer, Laravel, Xcode, Flutter,
Terraform, Pulumi, Helm, Ansible, just, mise, Gleam, Babashka, scala-cli, Godot,
PlatformIO, and the static site generators (Hugo, Jekyll, Zola, MkDocs, Quarto).
Going through the registry also turned up three bugs that had been sitting there quietly:
- The
angulartype could never match anything. It listed two markers,angular.jsonand.angular-cli.json, and marker lists are ANDed - but a project has one or the other, never both. So the type had been dead since the dayangular.jsonwas added. pyproject.tomlwas checked beforedjango,python-poetry,python-pipenvandpython-tox. Since just about every Python project has apyproject.tomlthese days, those four types effectively never matched. A Django project would come out aspython-tomland lose its file kinds along with its commands.php-symfonyrequired anappdirectory, which Symfony dropped in version 3, and avendordirectory, which only exists after someone runs composer. Modern Symfony projects weren’t detected at all.
I’ve also brought the stale markers up to date (Bazel’s MODULE.bazel, Gradle’s Kotlin
DSL, Zig’s build.zig) and fixed a pile of lifecycle commands that had rotted - Symfony’s
console moved to bin/ years ago, the Dart CLI absorbed pub, setup.py build is
deprecated by the PyPA, and poetry projects run pytest rather than unittest discovery.
There’s a new (:any "file1" "file2") form for markers, which is what made most of this
expressible as plain data rather than yet another predicate function.1
Monorepos
If you work in a monorepo you know the problem: the repository is one project as far as git (and therefore Projectile) is concerned, but you’re usually working on one piece of it, and building or testing the whole thing is a waste of time.
Projectile now understands that a directory holding a manifest of its own is a subproject.
projectile-find-file-in-subproject (s-p c m f) asks which one you want and then
scopes file completion to it:

And every lifecycle command has a subproject variant now - s-p c m c compiles, s-p c m t
tests, s-p c m r runs, and so on. They use the project’s own command and simply run it in
the nearest subproject, which is exactly what workspace-aware tools like Cargo, npm and
Maven need in order to scope their work.
There were subproject compile and test commands before this release, but they only looked
for the marker of the project’s own type, which meant they were useless in a polyglot
repository - a Rust crate sitting under a package.json project was invisible to them. Now
the markers come from every registered project type, so it works regardless of what the repo
looks like from the top.
Two commands for looking at a project
projectile-dashboard (s-p P) summarizes the project you’re in:

Everything in there is a button, so you can jump to a file, run a task or kick off a build
without leaving the buffer. It never indexes the project and only queries git when the
project is a local git repo, which makes it cheap enough to use as your
projectile-switch-project-action - which is exactly what I do now.
The other one is projectile-doctor (s-p H), which explains how Projectile sees the
current project:

Root, which detection function found it and on which marker, the type and its marker, the
indexing method and the exact command that will run, which external tools are available, the
cache state, the effective ignore rules - and then a list of findings, each either ok or a
concrete suggestion. It’s plain text, meant to be pasted into a bug report. Selfishly, I’m
hoping this saves us both some back-and-forth in the issue tracker.
Odds and ends
A few smaller things worth knowing about:
projectile-todos(s-p s t) collects the project’sTODO/FIXMEannotations into the reviewable search buffer.- Project-scoped bookmarks -
s-p B s,s-p B j,s-p B d. They’re plain Emacs bookmarks, Projectile just scopes the completion to the current project. projectile-replace-undo(s-p u) reverts the last replace you applied from the review buffer, and it’s careful about it - a file whose text has changed since is reported and left alone.projectile-per-command-compilation-buffergives compile, test and run separate compilation buffers, so running the tests no longer discards your build output.- An optional Embark and Marginalia integration, wired up with
with-eval-after-loadso neither package becomes a dependency.
Bug fixes
Three worth calling out.
The first is embarrassing and recent: with asynchronous indexing enabled, projectile-find-file
could show “Projectile is indexing” and spin forever. Projectile was waiting on the indexing
process’s sentinel, and Emacs doesn’t actually promise to run one while a command sits
waiting on the process. It now waits on the process itself and collects the output on its
own if the sentinel doesn’t show up.2 Thanks to Mike Crowe for a bug report with enough
detail in it (strace output included) that I could work backwards to the cause.
The second: in CMake projects, editing the compile command at the prompt didn’t stick, and you’d get the default offered again on the next run. That was a side effect of a fix from 3.2 that made function-valued commands re-resolve on every run so the preset picker could prompt again. It threw away your edit along with the cached command. Both behaviors work now - your edit is remembered, the picker still gets to prompt.
The third one has been open since 2016. If you opened a file that git ignores, Projectile
would happily add it to the file cache, and it would then keep showing up in
projectile-find-file even though indexing had correctly left it out. Projectile’s own
ignore rules simply don’t know anything about .gitignore, so it now asks git directly
before caching a file it hasn’t seen before.
Upgrade notes
A few changes might alter what you see, so it’s worth knowing about them before you upgrade:
- Alien indexing now honors Projectile’s ignore rules, which it used to skip entirely. This
means alien projects will list fewer files than before. Set
projectile-alien-honors-ignorestonilif you want the old behavior. - The ignore configuration speaks gitignore patterns everywhere now, matched the same way by
every indexing method. Mostly this just works, but a leading
*used to mean “at any depth” and is now a plain wildcard, so*node_modulesshould becomenode_modules. projectile-globally-ignored-directoriesnow includes the build and dependency directories of the common ecosystems -node_modules,target,__pycache__,.venv,.next,.terraformand a couple of dozen more. Directories that projects sometimes do commit (vendor,dist,public,build) are deliberately not on the list.
The full changelog is here, and the manual is at docs.projectile.mx.
That’s all I have for you today. Keep hacking!
-
Gradle is a good example -
build.gradle,build.gradle.kts,settings.gradleandsettings.gradle.ktsall mark a Gradle project, and a project will have some of them but not all. ↩