Tooling & Formatting

Copy Markdown View Source

Mob ships with first-class support for mix format so that ~MOB sigils are formatted alongside the rest of your Elixir code. Generated projects include a .formatter.exs that wires this up automatically.

Setting up the formatter

Add Mob.Formatter to your .formatter.exs:

# .formatter.exs
[
  plugins: [Mob.Formatter],
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

Then run:

mix format

That's it. Every ~MOB sigil in your project is now formatted with the rest of your code in one pass.

What gets formatted

Indentation

Children are indented 2 spaces per nesting level, matching Elixir convention:

# Before
~MOB"""
<Column>
<Text text="Hello" />
<Button text="OK" on_tap={tap} />
</Column>
"""

# After
~MOB"""
<Column>
  <Text text="Hello" />
  <Button text="OK" on_tap={tap} />
</Column>
"""

Attribute wrapping

When a tag's attributes would exceed line_length, each attribute moves to its own line with the closing /> or > on a dedicated line:

# Short enough to stay inline
~MOB(<Button text="Save" on_tap={save_tap} />)

# Wraps when attributes are too long
~MOB"""
<Image
  source={assigns.avatar_url}
  width={120}
  height={120}
  corner_radius={:radius_pill}
  fill_width={false}
/>
"""

Expression children

{expr} slots are indented to match their sibling nodes:

~MOB"""
<Column>
  <Text text="Items" text_size={:xl} />
  {Enum.map(assigns.items, fn item ->
    ~MOB(<Text text={item} />)
  end)}
</Column>
"""

Configuration

Line length

Mob.Formatter respects the standard line_length option:

[
  plugins: [Mob.Formatter],
  line_length: 120,
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

The default is 98 characters.

CI enforcement

Add mix format --check-formatted to your CI pipeline to catch unformatted sigils in the same pass as unformatted Elixir:

mix format --check-formatted
mix credo --strict
mix test

How it works

Mob.Formatter implements the Mix.Tasks.Format behaviour introduced in Elixir 1.14. Elixir's formatter calls it for every ~MOB sigil it encounters, passing the raw sigil content. The formatter re-parses the content using the same NimbleParsec grammar as the sigil itself and emits normalised output.

If a sigil can't be parsed (e.g. the file is in a mid-edit incomplete state), the formatter returns the content unchanged rather than raising. mix format never breaks a file it can't fully understand.

See Mob.Formatter for the full API reference.

Credo

Projects generated by mix mob.new include Credo in dev/test dependencies. Run it with:

mix credo --strict

The generated .credo.exs is pre-configured to work well with Mob's conventions (the ~MOB sigil, module attribute patterns, etc.).