Configuration

In the typical style of Emacs, Projectile is extremely configurable. Pretty much every aspect of its behaviour can be tweaked or extended.

In this section we’ll go over some of the most common things you might want to fine-tune to make Projectile fit your workflow better.

Project indexing method

Projectile has three modes of operation - one is portable and is implemented in Emacs Lisp (therefore it’s native to Emacs and is known as the native indexing method) and the other two (hybrid and alien) rely on external commands like find, git, etc to obtain the list of files in a project.

The alien indexing method maximizes the speed of the hybrid indexing method. This means that Projectile will not do any processing or sorting of the files returned by the external commands and you’re going to get the maximum performance possible. This behaviour makes a lot of sense for most people, as they’d typically be putting ignores in their VCS config (e.g. .gitignore) and won’t care about any additional ignores/unignores/sorting that Projectile might also provide.

By default the alien method is used on all operating systems except Windows. Prior to Projectile 2.0 hybrid used to be the default (but to make things confusing hybrid used to be known as alien back then).

To force the use of native indexing in all operating systems:

(setq projectile-indexing-method 'native)

To force the use of hybrid indexing in all operating systems:

(setq projectile-indexing-method 'hybrid)

To force the use of alien indexing in all operating systems:

(setq projectile-indexing-method 'alien)

This can speed up Projectile in Windows significantly (especially on big projects). The disadvantage of this method is that it’s not well supported on Windows systems, as it requires setting up some Unix utilities there. If there’s a problem, you can always use native indexing mode.

Alien indexing

The alien indexing works in a pretty simple manner - it simply shells out to a command that returns the list of files within a project. For version-controlled projects by default Projectile will use the VCS itself to obtain the list of files. As an example, here is the command that Projectile uses for Git projects:

git ls-files -zco --exclude-standard

For every supported VCS there’s a matching Projectile defcustom holding the command to invoke for it:

Variable VCS

projectile-git-command

Git

projectile-hg-command

Mercurial

projectile-svn-command

Subversion

projectile-bzr-command

Bazaar

projectile-darcs-command

Darcs

projectile-fossil-command

Fossil

projectile-pijul-command

Pijul

projectile-sapling-command

Sapling

projectile-jj-command

Jujutsu

There are also two Git-specific commands for listing submodules and ignored files:

;; Command to list git submodules (set to nil to disable)
(setq projectile-git-submodule-command
  "git submodule --quiet foreach 'echo $displaypath' | tr '\\n' '\\0'")

;; Command to get git-ignored files
(setq projectile-git-ignored-command "git ls-files -zcoi --exclude-standard")
If you ever decide to tweak those keep in mind that the command should always be returning the list of files relative to the project root and the resulting file list should be 0-delimited (as opposed to newline delimited).

For non-VCS projects Projectile will invoke whatever is in projectile-generic-command. The default chooses fd when it’s installed and falls back to find:

;; Effective default value of projectile-generic-command, picked at load time:
;;   when fd is on PATH:
"fd . -0 --type f --color=never --strip-cwd-prefix"
;;   otherwise:
"find . -type f | cut -c3- | tr '\\n' '\\0'"
It’s a great idea to install fd which is much faster than find. If fd is found, projectile will use it as a replacement for find for non-VCS projects.
The find fallback does not exclude common build/cache directories (.git, node_modules, target, build, …); a non-VCS project under alien indexing on a host without fd will list everything. Either install fd, switch to hybrid indexing so projectile-globally-ignored-directories applies, or override projectile-generic-command with a tighter recipe.

By default, fd is also used inside Git repositories (instead of git ls-files), because git ls-files has the limitation that it lists deleted files until the deletions are staged. With fd, deleted files disappear from the listing immediately; with git ls-files, Projectile post-filters the listing against git ls-files -zd to hide deletions until they’re staged. You can control this with projectile-git-use-fd:

;; Disable fd in git repos (use git ls-files instead)
(setq projectile-git-use-fd nil)

You can also customize the path to the fd executable and the arguments passed to it:

;; Use a custom fd path
(setq projectile-fd-executable "/usr/local/bin/fd")

;; Customize the fd arguments used in git repos
(setq projectile-git-fd-args "-H -0 -E .git -tf --strip-cwd-prefix -c never")

Hybrid indexing

The hybrid method runs the same external command as alien and then post-processes the result with Projectile’s filtering rules. In other words, it’s alien plus a second pass that applies:

  • .projectile (dirconfig) ignore/keep/ensure entries

  • projectile-globally-ignored-files, projectile-globally-ignored-directories, projectile-globally-ignored-file-suffixes, and projectile-global-ignore-file-patterns

  • projectile-globally-unignored-files and projectile-globally-unignored-directories

  • The configured sort order from projectile-sort-order

This is the right choice when your VCS lists more files than you want Projectile to surface (e.g. you want to drop *.elc even though they’re checked in), or when you rely on .projectile to keep/ignore subtrees inside an otherwise large repository. It’s slower than alien because of the extra pass, but it remains substantially faster than native for big projects since the file list itself is still produced by the external tool.

Indexing method comparison

Not all Projectile features apply to every indexing method. The table below summarises which configuration knobs take effect under which mode:

Feature native hybrid alien

Project file listing speed

Slow (Elisp)

Fast

Fastest

Honors .projectile (dirconfig) +/-/! rules

Yes

Yes

No

Honors projectile-globally-ignored-files / -directories / -file-suffixes / projectile-global-ignore-file-patterns

Yes

Yes

No (rely on the VCS / fd / find ignores instead)

Honors projectile-globally-unignored-*

Yes

Yes

No

Sorts via projectile-sort-order

Yes

Yes

No (returned in the external tool’s order)

File caching enabled by default

Yes

No

No

Works on Windows out of the box

Yes

Requires Unix utilities

Requires Unix utilities

When alien is in use and a non-empty .projectile file is present, Projectile emits a one-time warning so the silent bypass doesn’t catch you off guard. Switch to hybrid (or native) if you need those rules applied; set projectile-warn-when-dirconfig-is-ignored to nil to silence.

Sorting

You can choose how Projectile sorts files by customizing projectile-sort-order.

If Alien indexing is set, files are not sorted by Projectile at all.

The default is to not sort files:

(setq projectile-sort-order 'default)

To sort files by recently opened:

(setq projectile-sort-order 'recentf)

To sort files by recently active buffers and then recently opened files:

(setq projectile-sort-order 'recently-active)

To sort files by modification time (mtime):

(setq projectile-sort-order 'modification-time)

To sort files by access time (atime):

(setq projectile-sort-order 'access-time)

Caching

Project files

Since indexing a big project is not exactly quick (especially in Emacs Lisp), Projectile supports caching of the project’s files. The caching is enabled by default whenever native indexing is enabled.

To enable caching unconditionally use this snippet of code:

(setq projectile-enable-caching t)

At this point you can try out a Projectile command such as s-p f (M-x projectile-find-file RET).

Running C-u s-p f will invalidate the cache prior to prompting you for a file to jump to.

Pressing s-p z will add the currently visited file to the cache for current project. Generally files created outside Emacs will be added to the cache automatically the first time you open them.

Normally the cache lasts for the duration of your Emacs session. If you want to cache to persist between Emacs sessions you should set this option to 'persistent.

(setq projectile-enable-caching 'persistent)

Now the project cache is persistent and will be preserved during Emacs restarts. Each project gets its own cache file, that will be placed in the root folder of the project. The name of the cache file is .projectile-cache.eld by default, but you can tweak it if you want to:

(setq projectile-cache-file "foo.eld")

The cache file will be loaded automatically in memory the first time you trigger a "find file" operation for the project it belongs to.

You can purge an individual file from the cache with M-x projectile-purge-file-from-cache or an entire directory with M-x projectile-purge-dir-from-cache.

Prior to Projectile 2.9 the cache for all projects was serialized to the same file. In Projectile 2.9 this was changed and now each project has its own cache file relative to the project’s root directory.

When projectile-mode is enabled Projectile will auto-update the project cache when files within are added or deleted from within Emacs. (this is achieved by file hooks) This behavior can be disabled like this:

(setq projectile-auto-update-cache nil)

You can also set the project files cache to expire after a given number of seconds:

;; Expire the project files cache after 5 minutes
(setq projectile-files-cache-expire (* 5 60))

By default projectile-files-cache-expire is nil, meaning the cache never expires automatically.

One last thing - the project cache will be auto-invalidated if you’re using .projectile and it’s last modification time is more recent than the time at which the cache file was last updated.

Background indexing

The first "find file" in a large or remote project has to index it. With the alien and hybrid indexing methods Projectile does this without freezing Emacs: the indexing command runs asynchronously and is awaited in a way that keeps redisplay going and leaves C-g live, so you can abort a slow index (for instance one stuck on an unresponsive remote host) instead of staring at a frozen editor. The resulting file list is exactly what the synchronous indexer would produce.

This is on by default and controlled by projectile-async-indexing:

;; index synchronously, as in older Projectile versions
(setq projectile-async-indexing nil)
It has no effect under native indexing (the Emacs Lisp directory walk can’t run off the main thread), in batch mode, or while a keyboard macro is executing - those index synchronously.

You can also warm the cache ahead of time, without blocking, using M-x projectile-index-project-async:

;; index the current project in the background
(projectile-index-project-async)

It runs the project’s indexing command via an asynchronous process and populates the same cache that a regular projectile-find-file reads, so a later "find file" finds the cache already warm instead of indexing on the spot. You could, for instance, warm a project right after switching to it:

(add-hook 'projectile-after-switch-project-hook
          #'projectile-index-project-async)
Background indexing only works with the alien and hybrid indexing methods - the native (pure Emacs Lisp) walk can’t run off the main thread. It also requires caching to be enabled, since the warmed result is stored in the project files cache.

The asynchronous building blocks it’s made of - projectile-files-via-ext-command-async and projectile-dir-files-alien-async - are public, so a streaming file finder (for example one built on top of consult) can drive Projectile’s indexing command asynchronously and feed results into its own UI. See the next section.

Integrating a streaming file finder

If you’d rather have your own asynchronous/streaming UI (à la fzf, consult or affe) list a project’s files instead of projectile-find-file, projectile-project-files-producer hands you everything you need to run Projectile’s own indexing command yourself:

(projectile-project-files-producer)
;; => (:directory "/path/to/project/"
;;     :vcs git
;;     :command "git ls-files -zco --exclude-standard"
;;     :separator "\0")

Run :command in :directory and split its output on :separator. A minimal (non-streaming) example built on the public asynchronous runner:

(defun my/project-find-file ()
  "Pick a file from the current project, indexing without blocking."
  (interactive)
  (let* ((producer (projectile-project-files-producer))
         (default-directory (plist-get producer :directory)))
    (projectile-files-via-ext-command-async
     default-directory (plist-get producer :command)
     (lambda (files _err)
       (find-file (expand-file-name (completing-read "File: " files)
                                    default-directory))))))

A real streaming finder would instead consume the process output incrementally (filtering as you type) rather than collecting it. For git projects, note that :command lists the main worktree only; if you want byte-for-byte the same set as projectile-find-file (submodule files folded in, deleted-but-unstaged files removed) drive projectile-dir-files-alien-async instead.

Bundled Consult integration

Projectile ships such a finder for Consult in projectile-consult.el. It’s an optional module with a soft dependency on consult (Projectile core never loads it), so you opt in by requiring it:

(require 'projectile-consult)
(define-key projectile-command-map (kbd "f") #'projectile-consult-find-file)

projectile-consult-find-file lists the project with Projectile’s own indexing command and streams the candidates into Consult as they arrive, so there’s no wait for the whole project to be indexed before you can start typing. Because it goes through projectile-project-files-producer, it stays VCS-aware and honours the project’s indexing configuration. The module requires Emacs 29.1+ (Consult’s floor) and may be split into a standalone package in the future.

File exists cache

Projectile does many file existence checks since that is how it identifies a project root. Normally this is fine, however in some situations the file system speed is much slower than usual and can make emacs "freeze" for extended periods of time when opening files and browsing directories.

The most common example would be interfacing with remote systems using TRAMP/ssh. By default all remote file existence checks are cached

To disable remote file exists cache that use this snippet of code:

(setq projectile-file-exists-remote-cache-expire nil)

To change the remote file exists cache expire to 10 minutes use this snippet of code:

(setq projectile-file-exists-remote-cache-expire (* 10 60))

You can also enable the cache for local file systems, that is normally not needed but possible:

(setq projectile-file-exists-local-cache-expire (* 5 60))

TRAMP performance

Projectile is TRAMP-aware and the hot paths cache aggressively, so once the caches are warm, navigating a remote project should feel similar to a local one. The first command on a fresh remote project pays a one-time setup cost (one VCS-detection listing, one project-root walk, one indexing shell-out); after that everything is served from memory until you invalidate it.

The following knobs are most relevant for remote use:

  • projectile-indexing-method - leave at the default (hybrid) or set to alien. Don’t use native over TRAMP: native indexing walks the tree with one directory-files call per subdirectory, which is one remote round-trip per subdirectory. hybrid and alien use a single git ls-files / fd shell-out.

  • projectile-enable-caching - enables the per-project file list cache. Set to 'persistent if you want the cache to survive Emacs restarts, so reopening a remote project doesn’t re-shell-out.

  • projectile-file-exists-remote-cache-expire (default 5 minutes) - TTL for cached remote file-existence results. Lower it if you frequently create new project markers (.projectile, .git, …​) on the remote and want them detected sooner; raise it if your remotes are stable and you’d rather avoid the round-trip.

After creating, removing, or moving a project marker on a remote, run projectile-discard-root-cache (or projectile-invalidate-cache) - both also clear the file-existence cache, so the new state takes effect on the next call instead of waiting for the TTL.

A few things that don’t require configuration but are worth knowing:

  • fd is auto-detected per remote host (cached for the session). The projectile-fd-executable defcustom is the local detection result and isn’t used remotely - whatever Projectile finds at executable-find time on the remote (fd or fdfind) is what runs there. No fd on the remote means the indexing path falls back to git ls-files.

  • If indexing fails (e.g. git or fd isn’t on the remote PATH), Projectile signals a user-error pointing at *projectile-files-errors* instead of returning an empty file list. Older versions silently returned nil, which manifested as unexplained empty completion lists or stringp, nil crashes downstream.

Using Projectile Commands Outside of Projects Directories

Normally, you’d be using Projectile’s commands from within some project directory. If, however, you invoke a command outside of a project, by default you’ll be prompted for a project to switch to. That behavior is controlled by projectile-require-project-root. You can make Projectile simply raise an error outside of Project folders like this:

(setq projectile-require-project-root t)

If you want Projectile to be usable in every directory (even without the presence of project file):

(setq projectile-require-project-root nil)

With this setting if you invoke Projectile outside of a project, the current directory will be considered by Projectile the project root.

This might not be a great idea if you start Projectile in your home folder for instance. :-)

Switching projects

By default, projectile does not include the current project in the list when switching projects. If you want to include the current project, customize variable projectile-current-project-on-switch.

When running projectile-switch-project (s-p p) and projectile-switch-open-project (s-p q) Projectile invokes the command specified in projectile-switch-project-action (by default it is projectile-find-file).

Invoking the command with a prefix argument (C-u s-p p or C-u s-p q) will trigger the Projectile dispatch menu (projectile-dispatch), which gives you quick access to most common commands you might want to invoke on a project.

Depending on your personal workflow and habits, you may prefer to alter the value of projectile-switch-project-action:

projectile-find-file

This is the default.

With this setting, once you have selected your project via Projectile’s completion system (see below), you will remain in the completion system to select a file to visit. projectile-find-file is capable of retrieving files in all sub-projects under the project root, such as Git submodules. Currently, only Git is supported. Support for other VCS will be added in the future.

Dispatch menu on project switch

If you often need to invoke a different action right after switching to a project, press C-u s-p p (a prefix argument to any switch-project command). After you pick a project, Projectile opens the projectile-dispatch menu so you can choose what to do in it.

This requires the optional transient dependency. Without it the prefix argument is ignored and the usual projectile-switch-project-action runs.

projectile-find-file-in-known-projects

Similar to projectile-find-file but lists all files in all known projects. Since the total number of files could be huge, it is beneficial to enable caching for subsequent usages.

projectile-find-file-dwim

If point is on a filepath, Projectile first tries to search for that file in project:

  • If it finds just a file, it switches to that file instantly. This works even if the filename is incomplete, but there’s only a single file in the current project that matches the filename at point. For example, if there’s only a single file named "projectile/projectile.el" but the current filename is "projectile/proj" (incomplete), projectile-find-file still switches to "projectile/projectile.el" immediately because this is the only filename that matches.

  • If it finds a list of files, the list is displayed for selecting. A list of files is displayed when a filename appears more than one in the project or the filename at point is a prefix of more than two files in a project. For example, if `projectile-find-file' is executed on a filepath like "projectile/", it lists the content of that directory. If it is executed on a partial filename like "projectile/a", a list of files with character 'a' in that directory is presented.

  • If it finds nothing, display a list of all files in project for selecting.

projectile-dired

(setq projectile-switch-project-action #'projectile-dired)

With this setting, once you have selected your project, the top-level directory of the project is immediately opened for you in a dired buffer.

projectile-find-dir

(setq projectile-switch-project-action #'projectile-find-dir)

With this setting, once you have selected your project, you will remain in Projectile’s completion system to select a sub-directory of your project, and then that sub-directory is opened for you in a dired buffer. If you use this setting, then you will probably also want to set

(setq projectile-find-dir-includes-top-level t)

in order to allow for the occasions where you want to select the top-level directory.

Known Projects

Projectile stores the list of known projects in a file on disk. You can customize its location:

(setq projectile-known-projects-file "~/.emacs.d/projectile-bookmarks.eld")

Ignoring projects

You can prevent certain projects from being added to the known projects list:

;; A list of projects to ignore
(setq projectile-ignored-projects '("/tmp/" "~/.emacs.d/"))

For more flexible filtering, set projectile-ignored-project-function to a predicate that receives a project root and returns non-nil if the project should be ignored:

;; Ignore all remote (TRAMP) projects
(setq projectile-ignored-project-function #'file-remote-p)

Automatic tracking

By default, Projectile automatically adds projects to the known projects list whenever you visit a file in a project. You can disable this:

(setq projectile-track-known-projects-automatically nil)

Completion Options

Projectile reads with Emacs’s built-in completing-read, so it works out of the box with whatever minibuffer UI you use - Vertico, Consult + Marginalia, fido-mode/icomplete, Ido’s ido-ubiquitous, and so on. Its candidates are tagged with the project-file completion category, which Marginalia and Embark use to enhance how they’re presented.

;; the default; nothing to configure for the common case
(setq projectile-completion-system 'default)
Earlier versions had dedicated ido, ivy and helm values (and an auto mode that detected ido-mode/ivy-mode/helm-mode). These were removed - those frameworks are used through completing-read nowadays, or through their own Projectile integration packages, helm-projectile and counsel-projectile. Any of the old values now simply behaves like default.

Custom completion function

You can also set projectile-completion-system to a function that accepts a prompt and a list of choices:

(setq projectile-completion-system #'my-custom-completion-fn)
(setq projectile-completion-system
      (lambda (prompt choices)
        ;; ...
        ))

An example of a custom completion function is this one, which only shows the file name (not including path) and, if the selected file is not unique, follows up with a completion over names relative to the project root.

Project-specific Compilation Buffers

This affects all commands built on top of projectile—​run-project-cmd like:

  • projectile-configure-project

  • projectile-run-project

  • projectile-test-project

  • projectile-install-project

  • projectile-package-project

Normally, the buffers created by those commands would be shared (overwritten) between projects, but it’s also possible to make the compilation buffer names project-specific. This requires that the user set:

(setq projectile-per-project-compilation-buffer t)

Both of these degrade properly when not inside a project.

Limit the number of project file buffers

Projectile can be configured to keep a maximum number of file buffers of a project that are opened at one point. The custom variable projectile-max-buffer-count can be set to an integer that will be the buffer count cap. If this limit is reached, by opening a new file, Projectile will close the least recent buffer of the current project. If the variable is nil, there will be no cap on the buffer count.

(setq projectile-max-file-buffer-count 10)

Note that special project buffers (e.g. compilation, dired, etc) are not affected by this setting.

Mode line indicator

By default the minor mode indicator of Projectile appears in the form " Projectile[ProjectName:ProjectType]". This is configurable via several custom variables:

  • projectile-mode-line-prefix (by default " Projectile") controls the static part of the mode-line

  • projectile-dynamic-mode-line (by default t) controls whether to display the project name & type part of the mode-line

  • projectile-mode-line-function (by default projectile-default-mode-line) controls the actual function to be invoked to generate the mode-line. If you’d like to show different info you should supply a custom function to replace the default, for example (setq projectile-mode-line-function '(lambda () (format " Proj[%s]" (projectile-project-name))))

The project name & type will not appear when editing remote files (via TRAMP), as recalculating the project name is a fairly slow operation there and would slow down a bit opening the files. They will also not appear for non-file buffers, as they get updated via find-file-hook.

Searching

projectile-search (s-p s s) searches the project using a pluggable backend. Projectile ships three backends - grep, ripgrep and ag - and you pick which one projectile-search uses via projectile-search-backend:

;; `auto' (the default) picks the first available backend, favouring ripgrep,
;; then grep (which is always available).  You can also name a specific backend
;; or use `prompt' to be asked each time.
(setq projectile-search-backend 'ripgrep)

The dedicated commands projectile-grep (s-p s g), projectile-ripgrep (s-p s r) and projectile-ag (s-p s a) still exist; they’re just projectile-search with a forced backend.

For the grep backend, in Git projects you can use vc-git-grep instead of rgrep, which tends to be faster as it respects .gitignore:

(setq projectile-use-git-grep t)

Adding your own search backend

Register a backend with projectile-register-search-backend. It takes a name, a :description, an optional :available predicate, and a :search function called with the search term and a flag indicating whether it’s a regexp. For example, to add deadgrep:

(projectile-register-search-backend 'deadgrep
  :description "deadgrep"
  :available (lambda () (require 'deadgrep nil t))
  :search (lambda (term _regexp)
            (deadgrep term (projectile-acquire-root))))

(setq projectile-search-backend 'deadgrep)

Shells, REPLs and terminals

projectile-run (s-p x r) opens a shell, REPL or terminal in the project root using a pluggable backend, the same way projectile-search works. Projectile ships shell, eshell, ielm, term, vterm, eat and ghostel backends; projectile-shell-backend selects which one is used (default eshell):

(setq projectile-shell-backend 'vterm)

The dedicated commands (projectile-run-eshell, projectile-run-vterm, …​, plus the -other-window variants) are kept; they’re just projectile-run with a forced backend.

Register your own terminal with projectile-register-shell-backend - it takes a name, a :description, an optional :available predicate, and a :run function called with a new-process flag (the prefix argument) and an other-window flag:

(projectile-register-shell-backend 'mistty
  :description "mistty"
  :available (lambda () (require 'mistty nil t))
  :run (lambda (_new-process _other-window)
         (let ((default-directory (projectile-acquire-root)))
           (mistty))))

The generic registry machinery behind both projectile-search and projectile-run (projectile-register-backend) is intended to be reused for other pluggable command families in the future.

Project name

The project name displayed in the mode line and used in buffer names is determined by projectile-project-name-function. The default implementation uses the project root directory name, but you can provide your own:

(setq projectile-project-name-function
      (lambda (project-root)
        (file-name-nondirectory
         (directory-file-name project-root))))
If the variable projectile-project-name is set (e.g. via .dir-locals.el), it takes precedence over the function.

Hooks

Projectile provides several hooks for integrating with your workflow:

;; Run after a file is opened via projectile-find-file
(add-hook 'projectile-find-file-hook #'my-find-file-setup)

;; Run after a directory is opened via projectile-find-dir
(add-hook 'projectile-find-dir-hook #'my-find-dir-setup)

;; Run right before switching projects
(add-hook 'projectile-before-switch-project-hook #'my-before-switch-setup)

Miscellaneous

Verbose messages

By default Projectile echoes informational messages (not just errors). To suppress them:

(setq projectile-verbose nil)

Projectile adds a menu to the Emacs menu bar by default. To disable it:

(setq projectile-show-menu nil)

Project-type-specific Configuration

CMake

Projectile supports CMake presets. Preset support is disabled by default, but can be enabled by setting projectile-enable-cmake-presets to non-nil. With preset-support enabled Projectile will parse the preset files and present the command-specific presets when executing a lifecycle command. In addition a no preset option is included for entering the command manually.

Preset support requires a CMake version that supports preset and for json-parse-buffer to be available.