Current section

Files

Jump to
pretty_arrows README.md
Raw

README.md

# Pretty Arrows

A `mix format` plugin that lines up the parts of your code that are easiest to
read in columns: the arrows in `with` clauses, the options in Ecto `field`
declarations, and the arrows in one-line `case` clauses.

## Installation

Add `pretty_arrows` to your dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:pretty_arrows, "~> 0.4.0", only: [:dev, :test], runtime: false}
  ]
end
```

Then fetch it:

```sh
mix deps.get
```

## Setup

Add the plugin to the `:plugins` list in `.formatter.exs`:

```elixir
[
  plugins: [PrettyArrows],
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
```

`mix format` now aligns `with` clauses and Ecto `field` declarations.

## Plugins

| Module               | Aligns                             | Options                |
| -------------------- | ---------------------------------- | ---------------------- |
| `PrettyArrows`       | `with` clauses, Ecto `field`s, `case` clauses | `:with`, `:ecto`, `:case` |
| `WithFormatter`      | `with` clauses only                | `:with`                |
| `EctoFieldFormatter` | Ecto `field`s only                 | `:ecto`                |
| `CaseFormatter`      | `case` clauses only                | `:case`                |

Use `PrettyArrows` for all three alignments. Use one of the others if you only
want that feature. Each plugin reads only its own section of `:pretty_arrows`.

## Examples

### `with` clauses

Input:

```elixir
with a_result <- a_func(),
     {:ok, b_result_with_a_longer_name} <- b_func(),
     c <- c_func() do
  {:ok, c}
end
```

Output, with the default `padding: :left`:

```elixir
with                           a_result <- a_func(),
     {:ok, b_result_with_a_longer_name} <- b_func(),
                                      c <- c_func() do
  {:ok, c}
end
```

Set `padding: :right` to pad after each pattern instead:

```elixir
with a_result                           <- a_func(),
     {:ok, b_result_with_a_longer_name} <- b_func(),
     c                                  <- c_func() do
  {:ok, c}
end
```

```elixir
[
  plugins: [PrettyArrows],
  pretty_arrows: [with: [padding: :right]]
]
```

### Ecto fields

Input:

```elixir
schema "users" do
  field :id, :uuid
  field :name, :string
  field :nowish, :datetime
  field :c, :enum
end
```

Output, with the defaults (`parens: false`, `columns: 2`):

```elixir
schema "users" do
  field :id,     :uuid
  field :name,   :string
  field :nowish, :datetime
  field :c,      :enum
end
```

Keep the parentheses around the arguments:

```elixir
[
  plugins: [PrettyArrows],
  pretty_arrows: [ecto: [parens: true]]
]
```

Align more columns. With `columns: 3`, the options after the type line up too:

```elixir
schema "users" do
  field :id,         :uuid,                    primary_key: true
  field :MinCreated, MyType.Util.Type.Integer, primary_key: true
  field :name,       :string,                  null: false
end
```

```elixir
[
  plugins: [PrettyArrows],
  pretty_arrows: [ecto: [columns: 3]]
]
```

Trailing `#` comments on a `field` line stay on that line, and comment lines
between fields don't break the alignment.

### `case` clauses

Two or more one-line clauses in a `case` block get their `->` arrows lined up.
Clauses whose body is on the following lines are left alone.

Input:

```elixir
case my_fun() do
  {:ok, result_a} -> MyMod.a_fun(result_a)
  {:ok, results_for_b} -> MyMod.b_other_fun(results_for_b)
  {:error, reason} -> MyMod.error_handler(reason)
end
```

Output:

```elixir
case my_fun() do
  {:ok, result_a}      -> MyMod.a_fun(result_a)
  {:ok, results_for_b} -> MyMod.b_other_fun(results_for_b)
  {:error, reason}     -> MyMod.error_handler(reason)
end
```

```elixir
[
  plugins: [PrettyArrows]
]
```

Use `CaseFormatter` as the plugin to get only this alignment. To keep `PrettyArrows`
but turn off `case` alignment, set `pretty_arrows: [case: [enabled: false]]`.

Only code is aligned. Lines inside heredocs, arrows inside string literals, and
`->` in `fn` bodies inside a clause don't count as clauses.

## Configuration

`PrettyArrows` reads all of its options from a single `:pretty_arrows` key.
Every option is shown with its default:

```elixir
[
  plugins: [PrettyArrows],
  pretty_arrows: [
    with: [
      enabled: true,      # align `with` clauses
      padding: :left      # :left or :right
    ],
    ecto: [
      enabled: true,      # align Ecto `field`s
      parens: false,      # keep parentheses around field arguments
      columns: 2          # how many argument columns to align
    ],
    case: [
      enabled: true       # align one-line `case` clause arrows
    ]
  ]
]
```

| Option             | Default | Accepted values    |
| ------------------ | ------- | ------------------ |
| `with: [enabled:]` | `true`  | `true`, `false`    |
| `with: [padding:]` | `:left` | `:left`, `:right`  |
| `ecto: [enabled:]` | `true`  | `true`, `false`    |
| `ecto: [parens:]`  | `false` | `true`, `false`    |
| `ecto: [columns:]` | `2`     | a positive integer |
| `case: [enabled:]` | `true`  | `true`, `false`    |

Unknown options and invalid values raise an `ArgumentError` that names the
offending key, so a typo fails the format run instead of being ignored:

```text
** (ArgumentError) unknown option(s) [:padding_typo] in pretty_arrows.with, expected one of [:enabled, :padding]
```

## How it works

The plugin first runs the standard `Code.format_string!/2` formatter, then
works on the formatted text one line at a time:

- For a `with` clause, it finds the `with` line and the `do` line that closes
  it, then pads the clause patterns so the `<-` arrows line up. Nested `with`
  blocks are aligned separately.
- For `case` blocks, it aligns the `->` of one-line clauses at the clause
  indentation. Heredocs and string literals are skipped.
- For Ecto fields, it groups consecutive `field` lines, including comment lines
  between them, and pads each argument into its column. If an aligned line
  would exceed 98 characters, the remaining options move to a continuation
  line.

Heredoc contents (`"""` blocks) are never aligned, so `with`, `field`, and
`case` text inside test fixtures is left as written.

Because it works on the text line by line, each clause or field must start on
its own line for it to align. Layouts that put several clauses on one line are
formatted by `mix format` but not aligned.

## Migrating from 0.2.x

Version 0.3.0 moves every option under `:pretty_arrows` and removes the old
spellings. Old top-level keys raise an error that points here.

| 0.2.x                                                           | 0.3.0                                                              |
| --------------------------------------------------------------- | ------------------------------------------------------------------ |
| `with_padding: :right`                                          | `pretty_arrows: [with: [padding: :right]]`                         |
| `with: false`                                                   | `pretty_arrows: [with: [enabled: false]]`                          |
| `ecto: false`                                                   | `pretty_arrows: [ecto: [enabled: false]]`                          |
| `ecto: [parens: true]`                                          | `pretty_arrows: [ecto: [parens: true]]`                            |
| `ecto: [columns: 3]`                                            | `pretty_arrows: [ecto: [columns: 3]]`                              |
| `prettyarrows: [with: [padding: :right], ecto: [parens: true]]` | `pretty_arrows: [with: [padding: :right], ecto: [parens: true]]`   |
| `ecto: [fields: [parens: true]]`                                | `pretty_arrows: [ecto: [parens: true]]`                            |

String values such as `padding: "right"` and `enabled: "false"` are no longer
accepted. Use atoms and booleans.

## License

MIT