about summary refs log tree commit diff stats
path: root/content/post/opening-projects-with-projectile.md
blob: 43ee6f12f9a0121ffe8d9d9d8fd00e161bd06bed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
+++
Categories = ["Development", "Emacs"]
Description = ""
Tags = ["Development", "emacs", "lisp"]
title = "Opening Projects with Projectile"
date = 2014-07-12T09:12:34Z
+++

I use [Projectile] for working with projects in Emacs.  It's really good at finding files in projects, working with source code indexes (I use [Global]), and with its [perspective] support, it's also great at separating projects into workspaces.  However, I've always felt it lacking in actually opening projects.  I tend to work on different projects all the time and `projectile-switch-project` only tracks projects once they've been opened initially (despite the name, it works across Emacs sessions).

With this in mind, I decided to try to add support for opening projects under a given subdirectory, e.g. `~/projects`, regardless of whether or not I've visited them before.

I saw that projectile uses [Dash.el] in some places, and after reading about [anaphoric macros], I decided that I'd try to use them to aid me.

{{% highlight cl %}}
(defun ap/subfolder-projects (dir)
  (--map (file-relative-name it dir)
         (-filter (lambda (subdir)
                    (--reduce-from (or acc (funcall it subdir)) nil
                                   projectile-project-root-files-functions))
                  (-filter #'file-directory-p (directory-files dir t "\\<")))))
{{% /highlight %}}

First, this filters the non-special files under `dir`, filtering non-directories.  Then it runs the list of `projectile-project-root-files-functions` on it to determine if it looks like a projectile project.  To make the list more readable, it makes the filenames relative to the passed-in directory.  It runs like this:
                  
{{% highlight cl %}}
(ap/subfolder-projects "~/projects") =>
("dotfiles" "ggtags" …)
{{% /highlight %}}

So, we've got ourselves a list, but now we need to be able to open the project that's there, even though the folders are relative.

{{% highlight cl %}}
(defun ap/open-subfolder-project (from-dir &optional arg)
  (let ((project-dir (projectile-completing-read "Open project: "
                                     (ap/subfolder-projects from-dir))))
    (projectile-switch-project-by-name (expand-file-name project-dir from-dir) arg)))
{{% /highlight %}}

By wrapping the call to `ap/subfolder-projects` in another function that takes the same directory argument, we can re-use the project parent directory and expand the selected project name into an absolute path before passing it to `projectile-switch-project-by-name`.

We get support for multiple completion systems for free, since projectile has a wrapper function that works with the default system, ido, [grizzl] and recently, [helm].

Then I defined some helper functions to make it easy to open work and home projects.

{{% highlight cl %}}
(defvar work-project-directory "~/work")
(defvar home-project-directory "~/projects")

(defun ap/open-work-project (&optional arg)
  (interactive "P")
  (ap/open-subfolder-project work-project-directory arg))

(defun ap/open-home-project (&optional arg)
  (interactive "P")
  (ap/open-subfolder-project home-project-directory arg))
{{% /highlight %}}

I could probably simplify this with a macro, but I'm not sure that there's much advantage in it.  I only have two project types right now, after all.

With this all set up, whenever I want to start working on a project I just type `M-x home RET` to call up the list.

I also considered trying to add all the projects under a directory to the projectile known project list.  I didn't find it quite as easy to use, but it's available below if anyone would prefer that style.

{{% highlight cl %}}
(defun ap/-add-known-subfolder-projects (dir)
  (-map #'projectile-add-known-project (--map (concat (file-name-as-directory dir) it) (ap/subfolder-projects dir))))

(defun ap/add-known-subfolder-projects ()
  (interactive)
  (ap/-add-known-subfolder-projects (ido-read-directory-name "Add projects under: ")))
{{% /highlight %}}

[Projectile]: https://github.com/bbatsov/projectile
[Dash.el]: https://github.com/magnars/dash.el
[Helm]: https://github.com/emacs-helm/helm
[Global]: https://www.gnu.org/software/global/
[Anaphoric macros]: https://en.wikipedia.org/wiki/Anaphoric_macro
[Perspective]: https://github.com/nex3/perspective-el
[Grizzl]: https://github.com/d11wtq/grizzl