Projects

Supported Project Types

One of the main goals of Projectile is to operate on a wide range of project types without the need for any configuration. To achieve this it contains a lot of project detection logic and project type specific logic.

Broadly speaking, Projectile identifies projects like this:

  • Directories that contain the special .projectile file

  • Directories under version control (e.g. a Git repo)

  • Directories that contain some project description file (e.g. a Gemfile for Ruby projects or pom.xml for Java maven-based projects)

While Projectile aims to recognize most project types out-of-the-box, it’s also extremely flexible configuration-wise, and you can easily alter the project detection logic.

If you’d like to override the default project detection functions you should check out projectile-project-root-functions. We’ll discuss how to tweak in more details later in the documentation.

Version Control Systems

Projectile considers most version-controlled repos to be a project. Out of the box Projectile supports:

  • Git

  • Mercurial

  • Bazaar

  • Subversion

  • CVS

  • Fossil

  • Darcs

  • Sapling

  • Jujutsu

File markers

Projectile considers many files to denote the root of a project. Usually those files are the configuration files of various build tools. Out of the box the following are supported:

File Project Type

rebar.config

Rebar project file

project.clj

Leiningen project file

build.boot

Boot-clj project file

deps.edn

Clojure CLI project file

SConstruct

Scons project file

default.nix

Nix project file

flake.nix

Nix flake project file

pom.xml

Maven project file

build.sbt

SBT project file

build.sc

Mill project file

gradlew

Gradle wrapper script

build.gradle

Gradle project file

.ensime

Ensime configuration file

Gemfile

Bundler file

requirements.txt

Pip file

setup.py

Setuptools file

tox.ini

Tox file

composer.json

Composer project file

Cargo.toml

Cargo project file

mix.exs

Elixir mix project file

stack.yaml

Haskell’s stack tool based project

dune-project

OCaml Dune project file

info.rkt

Racket package description file

DESCRIPTION

R package description file

TAGS

etags/ctags are usually in the root of project

GTAGS

GNU Global tags

configure.in

autoconf old style

configure.ac

autoconf new style

cscope.out

cscope

Makefile

Make

CMakeLists.txt

CMake

WORKSPACE

Bazel workspace file

debian/control

Debian package dpkg control file

There’s also Projectile’s own .projectile which serves both as a project marker and a configuration file. We’ll talk more about later in this section.

Adding Custom Project Types

If a project you are working on is recognized incorrectly or you want to add your own type of projects you can add following to your Emacs initialization code

(projectile-register-project-type 'npm '("package.json")
                                  :project-file "package.json"
				  :compile "npm install"
				  :test "npm test"
				  :run "npm start"
				  :test-suffix ".spec")

What this does is:

  1. add your own type of project, in this case npm package.

  2. add a list of files and/or folders in a root of the project that helps to identify the type, in this case it is only package.json. This can also be a function which takes a project root as argument and verifies whether that directory has the correct project structure for the type.

  3. add project-file, which is typically the primary project configuration file. In this case that’s package.json. The value can contain wildcards and/or be a list containing multiple project files to look for.

  4. add compile-command, in this case it is npm install.

  5. add test-command, in this case it is npm test.

  6. add run-command, in this case it is npm start.

  7. add test files suffix for toggling between implementation/test files, in this case it is .spec, so the implementation/test file pair could be service.js/service.spec.js for example.

Let’s see a couple of more complex examples.

;; .NET C# or F# projects
(projectile-register-project-type 'dotnet #'projectile-dotnet-project-p
                                  :project-file '("?*.csproj" "?*.fsproj")
                                  :compile "dotnet build"
                                  :run "dotnet run"
                                  :test "dotnet test")

This example uses projectile-dotnet-project-p to validate the project’s structure. Since C# and F# project files have names containing the name of the project, it uses a list of wildcards to specify the different valid project-file name patterns.

;; Ruby + RSpec
(projectile-register-project-type 'ruby-rspec '("Gemfile" "lib" "spec")
                                  :project-file "Gemfile"
                                  :compile "bundle exec rake"
                                  :src-dir "lib/"
                                  :test "bundle exec rspec"
                                  :test-dir "spec/"
                                  :test-suffix "_spec")

;; Ruby + Minitest
(projectile-register-project-type 'ruby-test '("Gemfile" "lib" "test")
                                  :project-file "Gemfile"
                                  :compile "bundle exec rake"
                                  :src-dir "lib/"
                                  :test "bundle exec rake test"
                                  :test-suffix "_test")

;; Rails + Minitest
(projectile-register-project-type 'rails-test '("Gemfile" "app" "lib" "db" "config" "test")
                                  :project-file "Gemfile"
                                  :compile "bundle exec rails server"
                                  :src-dir "lib/"
                                  :test "bundle exec rake test"
                                  :test-suffix "_test")

;; Rails + RSpec
(projectile-register-project-type 'rails-rspec '("Gemfile" "app" "lib" "db" "config" "spec")
                                  :project-file "Gemfile"
                                  :compile "bundle exec rails server"
                                  :src-dir "lib/"
                                  :test "bundle exec rspec"
                                  :test-dir "spec/"
                                  :test-suffix "_spec")

All those projects are using Gemfile (bundler's project file), but they have different directory structures.

Below is a listing of all the available options for projectile-register-project-type:

Option Documentation

:project-file

A file, relative to the project root, typically the main project file (e.g. pom.xml for Maven projects).

:compilation-dir

A path, relative to the project root, from where to run the tests and compilation commands.

:compile

A command to compile the project.

:configure

A command to configure the project. %s will be substituted with the project root.

:install

A function to install the project.

:package

A function to package the project.

:run

A command to run the project.

:src-dir

A path, relative to the project root, where the source code lives. A function may also be specified which takes one parameter - the directory of a test file, and it should return the directory in which the implementation file should reside. This option is only used for implementation/test toggling.

:test

A command to test the project.

:test-dir

A path, relative to the project root, where the test code lives. A function may also be specified which takes one parameter - the directory of a file, and it should return the directory in which the test file should reside. This option is only used for implementation/test toggling.

:test-prefix

A prefix to generate test files names.

:test-suffix

A suffix to generate test files names.

:related-files-fn

A function to specify test/impl/other files in a more flexible way.

Returning Projectile Commands from a function

You can also pass a symbolic reference to a function into your project type definition if you wish to define the compile command dynamically:

(defun my/compile-command ()
  "Returns a String representing the compile command to run for the given context"
  (cond
   ((and (eq major-mode 'java-mode)
         (not (string-match-p (regexp-quote "\\.*/test/\\.*") (buffer-file-name (current-buffer)))))
    "./gradlew build")
   ((eq major-mode 'web-mode)
    "./gradlew compile-templates")
   ))

(defun my/test-command ()
  "Returns a String representing the test command to run for the given context"
  (cond
   ((eq major-mode 'js-mode) "grunt test") ;; Test the JS of the project
   ((eq major-mode 'java-mode) "./gradlew test") ;; Test the Java code of the project
   ((eq major-mode 'my-mode) "special-command.sh") ;; Even Special conditions/test-sets can be covered
   ))

(projectile-register-project-type 'has-command-at-point '("file.txt")
                                  :compile 'my/compile-command
                                  :test 'my/test-command)

If you would now navigate to a file that has the *.java extension under the ./tests/ directory and hit C-c c p you will see ./gradlew build as the suggestion. If you were to navigate to a HTML file the compile command will have switched to ./gradlew compile-templates.

This works for:

  • :configure

  • :compile

  • :compilation-dir

  • :run

Note that your function has to return a string to work properly.

The :test-prefix and :test-suffix will work regardless of file extension or directory path should and be enough for simple projects. The projectile-other-file-alist variable can also be set to find other files based on the extension.

For fine-grained control of implementation/test toggling, the :test-dir option of a project may take a function of one parameter (the implementation directory absolute path) and return the directory of the test file. This in conjunction with the options :test-prefix and :test-suffix will then be used to determine the full path of the test file. This option will always be respected if it is set.

Similarly, the :src-dir option, the analogue of :test-dir, may also take a function and exhibits exactly the same behaviour as above except that its parameter corresponds to the directory of a test file and it should return the directory of the corresponding implementation file.

It’s recommended that either both or neither of these options are set to functions for consistent behaviour.

Alternatively, for flexible file switching across a range of projects, the :related-files-fn option set to a custom function or a list of custom functions can be used. The custom function accepts the relative file name from the project root and it should return related file information as a plist with the following optional key/value pairs:

Key Value Command applicable

:impl

matching implementation file if the given file is a test file

projectile-toggle-between-implementation-and-test, projectile-find-related-file

:test

matching test file if the given file has test files.

projectile-toggle-between-implementation-and-test, projectile-find-related-file

:other

any other files if the given file has them.

projectile-find-other-file, projectile-find-related-file

:foo

any key other than above

projectile-find-related-file

For each value, following type can be used:

Type Meaning

string / a list of strings

Relative paths from the project root. The paths which actually exist on the file system will be matched.

a function

A predicate which accepts a relative path as the input and return t if it matches.

nil

No match exists.

Notes:

  1. For a big project consisting of many source files, returning strings instead of a function can be fast as it does not iterate over each source file.

  2. There is a difference in behaviour between no key and nil value for the key. Only when the key does not exist, other project options such as :test_prefix or projectile-other-file-alist mechanism is tried.

  3. If the :test-dir option is set to a function, this will take precedence over any value for :related-files-fn set when projectile-toggle-between-implementation-and-test is called.

Example - Same source file name for test and impl

(defun my/related-files (path)
  (if (string-match (rx (group (or "src" "test")) (group "/" (1+ anything) ".cpp")) path)
      (let ((dir (match-string 1 path))
            (file-name (match-string 2 path)))
        (if (equal dir "test")
            (list :impl (concat "src" file-name))
          (list :test (concat "test" file-name)
                :other (concat "src" file-name ".def"))))))

(projectile-register-project-type
   ;; ...
   :related-files-fn #'my/related-files)

With the above example, src/test directory can contain the same name file for test and its implementation file. For example, "src/foo/abc.cpp" will match to "test/foo/abc.cpp" as test file and "src/foo/abc.cpp.def" as other file.

Example - Different test prefix per extension

A custom function for the project using multiple programming languages with different test prefixes.

(defun my/related-files(file)
  (let ((ext-to-test-prefix '(("cpp" . "Test")
                              ("py" . "test_"))))
    (if-let ((ext (file-name-extension file))
             (test-prefix (assoc-default ext ext-to-test-prefix))
             (file-name (file-name-nondirectory file)))
        (if (string-prefix-p test-prefix file-name)
            (let ((suffix (concat "/" (substring file-name (length test-prefix)))))
              (list :impl (lambda (other-file)
                            (string-suffix-p suffix other-file))))
          (let ((suffix (concat "/" test-prefix file-name)))
            (list :test (lambda (other-file)
                          (string-suffix-p suffix other-file))))))))

projectile-find-related-file command is also available to find and choose related files of any kinds. For example, the custom function can specify the related documents with ':doc' key. Note that projectile-find-related-file only relies on :related-files-fn for now.

:related-files-fn can accept a list of custom functions to combine the result of each custom function. This allows users to write several custom functions and apply them differently to projects.

Projectile includes a couple of helpers to generate commonly used custom functions.

Helper name and params Purpose

groups KIND GROUPS

Relates files in each group as the specified kind.

extensions KIND EXTENSIONS

Relates files with extensions as the specified kind.

test-with-prefix EXTENSION PREFIX

Relates files with prefix and extension as :test and :impl.

test-with-suffix EXTENSION SUFFIX

Relates files with suffix and extension as :test and :impl.

Each helper means projectile-related-files-fn-helper-name function.

(setq my/related-files
      (list
       (projectile-related-files-fn-extensions :other '("cpp" "h" "hpp"))
       (projectile-related-files-fn-test-with-prefix "cpp" "Test")
       (projectile-related-files-fn-test-with-suffix "el" "_test")
       (projectile-related-files-fn-groups
        :doc
        '(("doc/common.txt"
           "src/foo.h"
           "src/bar.h")))))

(projectile-register-project-type
   ;; ...
   :related-files-fn my/related-files)

Editing Existing Project Types

You can also edit specific options of already existing project types:

(projectile-update-project-type
 'sbt
 :related-files-fn
 (list
  (projectile-related-files-fn-test-with-suffix "scala" "Spec")
  (projectile-related-files-fn-test-with-suffix "scala" "Test"))
 :test-prefix nil
 :precedence 'high)

This will change the value of the related-files-fn option, remove the test-prefix option and :precedence 'high sets the sbt project type to be chosen in preference to other potentially clashing project types (a value 'low would do the opposite).

Whilst setting the :test-dir and :src-dir to strings is sufficient for most purposes, using functions can give more flexibility. As an example consider (also using f.el):

(defun my-get-python-test-file (impl-file-path)
  "Return the corresponding test file directory for IMPL-FILE-PATH"
  (let* ((rel-path (f-relative impl-file-path (projectile-project-root)))
         (src-dir (car (f-split rel-path))))
    (cond ((f-exists-p (f-join (projectile-project-root) "test"))
           (projectile-complementary-dir impl-file-path src-dir "test"))
          ((f-exists-p (f-join (projectile-project-root) "tests"))
           (projectile-complementary-dir impl-file-path src-dir "tests"))
          (t (error "Could not locate a test file for %s!" impl-file-path)))))

(defun my-get-python-impl-file (test-file-path)
  "Return the corresponding impl file directory for TEST-FILE-PATH"
  (if-let* ((root (projectile-project-root))
            (rel-path (f-relative test-file-path root))
            (src-dir-guesses `(,(f-base root) ,(downcase (f-base root)) "src"))
            (src-dir (cl-find-if (lambda (d) (f-exists-p (f-join root d)))
                                 src-dir-guesses)))
      (projectile-complementary-dir test-file-path "tests?" src-dir)
    (error "Could not locate a impl file for %s!" test-file-path)))

(projectile-update-project-type
 'python-pkg
 :src-dir #'my-get-python-impl-dir
 :test-dir #'my-get-python-test-dir)

This attempts to recognise projects using both test and tests as top level directories for test files. An alternative using the related-files-fn option could be:

(projectile-update-project-type
 'python-pkg
 :related-files-fn
 (list
  (projectile-related-files-fn-test-with-suffix "py" "_test")
  (projectile-related-files-fn-test-with-prefix "py" "test_")))

In fact this is a lot more flexible in terms of finding test files in different locations, but will not create test files for you.

Customizing Project Detection

Project detection is pretty simple - Projectile just runs a list of project detection functions (projectile-project-root-functions) until one of them returns a project directory.

This list of functions is customizable, and while Projectile has some defaults for it, you can tweak it however you see fit.

Let’s take a closer look at projectile-project-root-functions:

(defcustom projectile-project-root-functions
  '(projectile-root-local
    projectile-root-marked
    projectile-root-bottom-up
    projectile-root-top-down
    projectile-root-top-down-recurring)
  "A list of functions for finding project roots."
  :group 'projectile
  :type '(repeat function))

The important thing to note here is that the functions get invoked in their order on the list, so the functions earlier in the list will have a higher precedence with respect to project detection. Let’s examine the defaults:

  • projectile-root-local looks for project path set via the buffer-local variable projectile-project-root. Typically you’d set this variable via .dir-locals.el and it will take precedence over everything else.

  • projectile-root-marked looks for .projectile (or whatever you’ve set as the value of projectile-dirconfig-file). The idea is that normally if you have a .projectile file you’d like it to override the normal project root discovery logic.

  • projectile-root-bottom-up will start looking for a project marker file/folder(e.g. .projectile, .hg, .git) from the current folder (a.k.a. default-directory in Emacs lingo) up the directory tree. It will return the first match it discovers. The assumption is pretty simple - the root marker appear only once, at the root folder of a project. If a root marker appear in several nested folders (e.g. you’ve got nested git projects), the bottom-most (closest to the current dir) match has precedence. You can customize the root markers recognized by this function via projectile-project-root-files-bottom-up

  • projectile-root-top-down is similar, but it will return the top-most (farthest from the current directory) match. It’s configurable via projectile-project-root-files and all project manifest markers like pom.xml, Gemfile, project.clj, etc go there.

  • projectile-root-top-down-recurring will look for project markers that can appear at every level of a project (e.g. Makefile or .svn) and will return the top-most match for those.

The default ordering should work well for most people, but depending on the structure of your project you might want to tweak it.

Re-ordering those functions will alter the project detection, but you can also replace the list. Here’s how you can delegate the project detection to Emacs’s built-in function vc-root-dir:

;; we need this wrapper to match Projectile's API
(defun projectile-vc-root-dir (dir)
  "Retrieve the root directory of the project at DIR using `vc-root-dir'."
  (let ((default-directory dir))
    (vc-root-dir)))

(setq projectile-project-root-functions '(projectile-vc-root-dir))

Similarly, you can leverage the built-in project.el like this:

;; we need this wrapper to match Projectile's API
(defun projectile-project-current (dir)
  "Retrieve the root directory of the project at DIR using `project-current'."
  (cdr (project-current nil dir)))

(setq projectile-project-root-functions '(projectile-project-current))

Ignoring files

The contents of .projectile are ignored when using the alien project indexing method.

If you’d like to instruct Projectile to ignore certain files in a project, when indexing it you can do so in the .projectile file by adding each path to ignore, where the paths all are relative to the root directory and start with a slash. Everything ignored should be preceded with a - sign. Alternatively, not having any prefix at all also means to ignore the directory or file pattern that follows. Here’s an example for a typical Rails application:

-/log
-/tmp
-/vendor
-/public/uploads

This would ignore the folders only at the root of the project. Projectile also supports relative pathname ignores:

-tmp
-*.rb
-*.yml
-models

You can also ignore everything except certain subdirectories. This is useful when selecting the directories to keep is easier than selecting the directories to ignore, although you can do both. To select directories to keep, that means everything else will be ignored.

Example:

+/src/foo
+/tests/foo

Keep in mind that you can only include subdirectories, not file patterns.

If both directories to keep and ignore are specified, the directories to keep first apply, restricting what files are considered. The paths and patterns to ignore are then applied to that set.

Finally, you can override ignored files. This is especially useful when some files ignored by your VCS should be considered as part of your project by projectile:

!/src/foo
!*.yml

When a path is overridden, its contents are still subject to ignore patterns. To override those files as well, specify their full path with a bang prefix.

If you would like to include comment lines in your .projectile file, you can customize the variable projectile-dirconfig-comment-prefix. Assigning it a non-nil character value, e.g. #, will cause lines in the .projectile file starting with that character to be treated as comments instead of patterns.

File-local project root definitions

If you want to override the projectile project root for a specific file, you can set the file-local variable projectile-project-root. This can be useful if you have files within one project that are related to a different project (for instance, Org files in one git repo that correspond to other projects).

Storing project settings

From project to project, some things may differ even in the same language - coding styles, auto-completion sources, etc. If you need to set some variables according to the selected project, you can use a standard Emacs feature called Per-directory Local Variables. To use it you must create a file named .dir-locals.el (as specified by the constant dir-locals-file) inside the project directory. This file should contain something like this:

((nil . ((secret-ftp-password . "secret")
         (compile-command . "make target-x")
         (eval . (progn
                   (defun my-project-specific-function ()
                     ;; ...
                     )))))
 (c-mode . ((c-file-style . "BSD"))))

The top-level alist member referenced with the key nil applies to the entire project. A key with the name eval will evaluate its corresponding value. In the example above, this is used to create a function. It could also be used to e.g. add such a function to a key map.

You can also quickly visit or create the dir-locals-file with s-p E (M-x projectile-edit-dir-locals RET). 3rd party packages may use functions projectile-add-dir-local-variable and projectile-delete-dir-local-variable to store their settings.

Here are a few examples of how to use this feature with Projectile.

Configuring Projectile’s Behavior

Projectile exposes many variables (via defcustom) which allow users to customize its behavior. Directory variables can be used to set these customizations on a per-project basis.

You could enable caching for a project in this way:

((nil . ((projectile-enable-caching . t))))

If one of your projects had a file that you wanted Projectile to ignore, you would customize Projectile by:

((nil . ((projectile-globally-ignored-files . ("MyBinaryFile")))))

If you wanted to wrap the git command that Projectile uses to list the files in you repository, you could do:

((nil . ((projectile-git-command . "/path/to/other/git ls-files -zco --exclude-standard"))))

If you want to use a different project name than how Projectile named your project, you could customize it with the following:

((nil . ((projectile-project-name . "your-project-name-here"))))

By default, compilation buffers are not writable, which allows you to e.g. press g to restart the last command. Setting projectile-<cmd>-use-comint-mode (where <cmd> is configure, compile, test, install, package, or run) to a non-nil value allows you to make projectile compilation buffers interactive, letting you e.g. test a command-line program with projectile-run-project.

(setq projectile-comint-mode t)

Project Buffers

Projectile offers a bunch of operations that are operating on the open buffers for some project (e.g. projectile-kill-buffers). One tricky part here are "special buffers" - basically buffers that are not backed by files (e.g. *dired*, +scratch+ and so on). Projectile determines whether a special buffer belongs to a project simply by checking the default-directory for the special buffer, which admittedly might result in some weird results (e.g. if you’ve created a special buffer that’s not related to a project, while visiting a file belonging to the project).

That’s why Projectile has a couple of configuration options for dealing with project buffers - namely projectile-globally-ignored-buffers and projectile-globally-ignored-modes. Both of them take a list of strings or regular expressions that will be used to match against a buffer’s name or a buffer’s major mode.

Here are a couple of examples:

;; ignoring specific buffers by name
(setq projectile-globally-ignored-buffers
  '("*scratch*"
    "*lsp-log*"))

;; ignoring buffers by their major mode
(setq projectile-globally-ignored-modes
  '("erc-mode"
    "help-mode"
    "completion-list-mode"
    "Buffer-menu-mode"
    "gnus-.*-mode"
    "occur-mode"))

Configure a Project’s Lifecycle Commands and Other Attributes

There are a few variables that are intended to be customized via .dir-locals.el.

  • for configuration - projectile-project-configure-cmd

  • for compilation - projectile-project-compilation-cmd

  • for testing - projectile-project-test-cmd

  • for installation - projectile-project-install-cmd

  • for packaging - projectile-project-package-cmd

  • for running - projectile-project-run-cmd

  • for configuring the test prefix - projectile-project-test-prefix

  • for configuring the test suffix - projectile-project-test-suffix

  • for configuring the related-files-fn property - projectile-project-related-files-fn

  • for configuring the src-dir property - projectile-project-src-dir

  • for configuring the test-dir property - projectile-project-test-dir

When these variables have their default value of nil, Projectile runs the default command for the current project type. You can override this behavior by setting them to either a string to run an external command or an Emacs Lisp function:

(setq projectile-test-cmd #'custom-test-function)

In addition caching of commands can be disabled by setting the variable projectile-project-enable-cmd-caching is to nil. This is useful for preset-based CMake projects.

By default, Projectile will not add consecutive duplicate commands to its command history. To alter this behaviour you can use projectile-cmd-hist-ignoredups. The default value of t means consecutive duplicates are ignore, a value of nil means nothing is ignored, and a value of 'erase' means only the last duplicate is kept in the command history.