If you came to Elixir from Rails-land, you might miss the navigation that came
with vim-rails. If you’re not familiar with it, vim-rails
creates commands
like :A
, :AV
, :AS
, and :AT
to quickly toggle between a source file and
its test file and commands like :Econtroller
, :Emodel
, and :Eview
to edit
files based on their type.
The good news is that the same person who made vim-rails
also made
vim-projectionist (thanks Tim
Pope). And with it, we can supercharge our navigation in Elixir and Phoenix just
like we had in Rails with vim-rails
.
Projecting Back to the Future
The easiest way to use vim-projectionist
is to set up projections in a
.projections.json
file at the root of your project. This is a basic file for
Elixir projections:
{
"lib/*.ex": {
"alternate": "test/{}_test.exs",
"type": "source"
},
"test/*_test.exs": {
"alternate": "lib/{}.ex",
"type": "test"
}
}
With this configuration, projectionist allows us to alternate between test and
source files using :A
, and it can open that alternate file in a separate pane
with :AS
or :AV
, or if you’re a tabs person, in a separate tab with :AT
.
Note that we define the "alternate"
both ways so that both the source and test
files have alternates.
If you’re wondering how it works, projectionist is grabbing any directory and
files matched by *
— from a globbing perspective it acts like **/*
— and expanding it with {}
. So the alternate of lib/project/sample.ex
is test/project/sample_test.exs
(and vice versa).
With that simple configuration, projectionist also defines two :E
commands
based on the "type"
:
:Esource project/sample
will openlib/project/sample.ex
, and:Etest project/sample
will opentest/project/sample_test.exs
.
Pretty neat, right? But wait! There’s more.
Templating
Projectionist has another really interesting feature — defining templates to use when creating files. Add the following templates to each projection:
{
"lib/*.ex": {
"alternate": "test/{}_test.exs",
"type": "source",
+ "template": [
+ "defmodule {camelcase|capitalize|dot} do",
+ "end"
+ ]
},
"test/*_test.exs": {
"alternate": "lib/{}.ex",
"type": "test",
+ "template": [
+ "defmodule {camelcase|capitalize|dot}Test do",
+ " use ExUnit.Case, async: true",
+ "",
+ " alias {camelcase|capitalize|dot}",
+ "end"
+ ]
}
}
The "template"
key takes an array of strings to use as the template. In them,
projectionist allows us to define a series of transformations that will act upon
whatever is captured by *
. We use {camelcase|capitalize|dot}
, so if *
captures project/super_random
, projectionist will do the following
transformations:
- camelcase:
project/super_random
->project/superRandom
, - capitalize:
project/superRandom
->Project/SuperRandom
, - dot:
Project/SuperRandom
->Project.SuperRandom
Example workflow
Let’s put it all together in a sample MiddleEarth
project.
We can create a new file via :Esource middle_earth/minas_tirith
. It will
create a file lib/middle_earth/minas_tirith.ex
with this template:
defmodule MiddleEarth.MinasTirith do
end
We can then create a test file by attempting to navigate to the (non-existing)
alternate file. Typing :A
will give us something like this:
Create alternate file?
1 /dev/middle_earth/test/middle_earth/minas_tirith_test.exs
Type number and <Enter> or click with mouse (empty cancels):
Typing 1
and <Enter>
will create the test file
test/middle_earth/minas_tirith_test.exs
with this template:
defmodule MiddleEarth.MinasTirithTest do
use ExUnit.Case, async: true
alias MiddleEarth.MinasTirith
end
Here it is in gif form:
Very cool, right? But wait. There’s more.
Supercharge Phoenix Navigation
That simple configuration works for Elixir projects. And since Phoenix projects
(beginning with Phoenix 1.3) have their files under lib/
, it also works okay
for Phoenix projects.
But without further changes, creating a Phoenix controller or a Phoenix channel
will gives us an extra Controllers
or Channels
namespace in our modules
because of the directory structure. For example, creating
lib/project_web/controllers/user_controller.ex
will create a module
ProjectWeb.Controllers.UserController
instead of the desired
ProjectWeb.UserController
.
It would also be nice to have controller-specific templates that include use
ProjectWeb, :controller
in controllers and use ProjectWeb.ConnCase
in
controller tests (since we always need those use
declarations). And, it would
be extra nice to have access to an :Econtroller
command.
We can make that happen by adding Phoenix-specific projections to our
.projections.json
file. Start with controllers:
{
"lib/**/controllers/*_controller.ex": {
"type": "controller",
"alternate": "test/{dirname}/controllers/{basename}_controller_test.exs",
"template": [
"defmodule {dirname|camelcase|capitalize}.{basename|camelcase|capitalize}Controller do",
" use {dirname|camelcase|capitalize}, :controller",
"end"
]
},
"test/**/controllers/*_controller_test.exs": {
"alternate": "lib/{dirname}/controllers/{basename}_controller.ex",
"type": "test",
"template": [
"defmodule {dirname|camelcase|capitalize}.{basename|camelcase|capitalize}ControllerTest do",
" use {dirname|camelcase|capitalize}.ConnCase, async: true",
"end"
]
},
# ... other projections
}
Note that these projections no longer use the single *
matcher for globbing.
They use **
and *
separately. And instead of simply using {}
in alternate
files, they explicitly use {dirname}
and {basename}
.
Why the change? Here’s what the projectionist documentation says:
For advanced cases, you can include both globs explicitly:
"test/**/test_*.rb"
. When expanding with{}
, the**
and*
portions are joined with a slash. If necessary, thedirname
andbasename
expansions can be used to split the value back apart.
Controller templates
By separating the globbing, we are able to create templates that do not include
the extra Controllers
namespace even though the path includes /controllers
.
We get the project name with **
, and we get the file name after /controllers
with *_controller.ex
. We then generate the namespace ProjectWeb
by grabbing
dirname
(i.e. project_web
) and putting it through a series of
transformations. Similarly, we generate the rest of the module’s name by using
basename
, putting it through a series of transformations, and appending either
Controller
or ControllerTest
.
We are also able to create more helpful controller templates since the
projections are specific to controllers. Note the inclusion of " use
{dirname|camelcase|capitalize}, :controller"
and " use
{dirname|camelcase|capitalize}.ConnCase, async: true"
in our templates. Our
controllers will now automatically include use ProjectWeb, :controller
and our
controller tests will automatically include use ProjectWeb.ConnCase, async:
true
.
:Econtroller command
Finally, we set the "type": "controller"
. That gives us the :Econtroller
command. We can now create a controller with :Econtroller project_web/user
.
And for existing controllers, projectionist has smart tab completion. So typing
:Econtroller user
and hitting tab should expand to :Econtroller
project_web/user
or give us more options if there are multiple matches.
For example, in the MiddleEarth
project we can edit the default
PageController
that ships with Phoenix by using :Econtroller page
along with
tab completion. And we can create a new MinasMorgul
controller and controller
test with our fantastic templates by typing :Econtroller
middle_earth_web/minas_morgul
and then going to its alternate file.
Projecting All the Things
I think you get the gist of it, so I will not go through all the projections. But just like we added the projections for the controllers, we can do the same for views, channels, and even feature tests if you frequently write those.
Below I included a sample file to get you started with controllers, views, channels, and feature tests. Take a look at it. If you prefer it in github-gist form, here’s a link to one. The best thing is that if my sample file does not fit your needs, you can always adjust it!
If you find any improvements, I would love to hear about them. I’m always looking for better ways to navigate files.
{
"lib/**/views/*_view.ex": {
"type": "view",
"alternate": "test/{dirname}/views/{basename}_view_test.exs",
"template": [
"defmodule {dirname|camelcase|capitalize}.{basename|camelcase|capitalize}View do",
" use {dirname|camelcase|capitalize}, :view",
"end"
]
},
"test/**/views/*_view_test.exs": {
"alternate": "lib/{dirname}/views/{basename}_view.ex",
"type": "test",
"template": [
"defmodule {dirname|camelcase|capitalize}.{basename|camelcase|capitalize}ViewTest do",
" use ExUnit.Case, async: true",
"",
" alias {dirname|camelcase|capitalize}.{basename|camelcase|capitalize}View",
"end"
]
},
"lib/**/controllers/*_controller.ex": {
"type": "controller",
"alternate": "test/{dirname}/controllers/{basename}_controller_test.exs",
"template": [
"defmodule {dirname|camelcase|capitalize}.{basename|camelcase|capitalize}Controller do",
" use {dirname|camelcase|capitalize}, :controller",
"end"
]
},
"test/**/controllers/*_controller_test.exs": {
"alternate": "lib/{dirname}/controllers/{basename}_controller.ex",
"type": "test",
"template": [
"defmodule {dirname|camelcase|capitalize}.{basename|camelcase|capitalize}ControllerTest do",
" use {dirname|camelcase|capitalize}.ConnCase, async: true",
"end"
]
},
"lib/**/channels/*_channel.ex": {
"type": "channel",
"alternate": "test/{dirname}/channels/{basename}_channel_test.exs",
"template": [
"defmodule {dirname|camelcase|capitalize}.{basename|camelcase|capitalize}Channel do",
" use {dirname|camelcase|capitalize}, :channel",
"end"
]
},
"test/**/channels/*_channel_test.exs": {
"alternate": "lib/{dirname}/channels/{basename}_channel.ex",
"type": "test",
"template": [
"defmodule {dirname|camelcase|capitalize}.{basename|camelcase|capitalize}ChannelTest do",
" use {dirname|camelcase|capitalize}.ChannelCase, async: true",
"",
" alias {dirname|camelcase|capitalize}.{basename|camelcase|capitalize}Channel",
"end"
]
},
"test/**/features/*_test.exs": {
"type": "feature",
"template": [
"defmodule {dirname|camelcase|capitalize}.{basename|camelcase|capitalize}Test do",
" use {dirname|camelcase|capitalize}.FeatureCase, async: true",
"end"
]
},
"lib/*.ex": {
"alternate": "test/{}_test.exs",
"type": "source",
"template": [
"defmodule {camelcase|capitalize|dot} do",
"end"
]
},
"test/*_test.exs": {
"alternate": "lib/{}.ex",
"type": "test",
"template": [
"defmodule {camelcase|capitalize|dot}Test do",
" use ExUnit.Case, async: true",
"",
" alias {camelcase|capitalize|dot}",
"end"
]
}
}