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:

The projectile-run-task prompt listing npm scripts and Makefile targets discovered in the project

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 angular type could never match anything. It listed two markers, angular.json and .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 day angular.json was added.
  • pyproject.toml was checked before django, python-poetry, python-pipenv and python-tox. Since just about every Python project has a pyproject.toml these days, those four types effectively never matched. A Django project would come out as python-toml and lose its file kinds along with its commands.
  • php-symfony required an app directory, which Symfony dropped in version 3, and a vendor directory, 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:

The Subproject prompt listing packages/ui, services/api and services/web

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:

The Projectile dashboard showing project info, version control status, recent files and tasks

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:

The Projectile doctor report showing project root, type, indexing method and available tools

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’s TODO/FIXME annotations 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-buffer gives 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-load so 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-ignores to nil if 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_modules should become node_modules.
  • projectile-globally-ignored-directories now includes the build and dependency directories of the common ecosystems - node_modules, target, __pycache__, .venv, .next, .terraform and 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!

  1. Gradle is a good example - build.gradle, build.gradle.kts, settings.gradle and settings.gradle.kts all mark a Gradle project, and a project will have some of them but not all. 

  2. See https://github.com/bbatsov/projectile/pull/2124