Steering Zig Fmt

Two tips on using zig fmt effectively. Read this if you are writing Zig, or if you are implementing a code formatter.

For me, zig fmt is better than any other formatter I used: rustfmt, the one in IntelliJ, deno fmt. zig fmt is steerable. For every syntactic construct, it has several variations for how it might be laid out. The variation used is selected by looking at what’s currently in a file.

Easier to show a pair of examples:

    f(1, 2,
      3);

// -> zig fmt ->

    f(1, 2, 3);
    f(1, 2,
      3,);

// -> zig fmt ->

    f(
        1,
        2,
        3,
    );

Depending on the trailing comma, function call is formatted on a single line, or with one argument per line.

The way this plays out in practice is that you decide how you want to lay out the code, add a couple of ,, hit the reformat shortcut (, p is mine), and zig fmt does the rest. For me, this works better than the alternative of the formatter guessing. 90% of great formatting are blank lines between logical blocks and tasteful choice of intermediate variables, so you might as well lean into key choices, rather than eliminate them.

I know of one non-trivial formatting customization point: columnar layout for arrays:

    .{ 1, 2, 3,
       4, 5, 6, 7, 8, 9, 10, 11,  };

One would think that trailing comma would lead to a number-per-line layout, but, for arrays, zig fmt also takes note of the first line break. In this case, the line break comes after the first three items, so we get three numbers per line, aligned:

    .{
        1,  2,  3,
        4,  5,  6,
        7,  8,  9,
        10, 11,
    };

How cool is that!

Furthermore, with judicious use of ++ (array concatenation), you can vary the number of items per line. When I need to pass --key value pairs to subprocess, I often go for formatting like this:

try run(&(.{ "aws", "s3", "sync", path, url } ++ .{
    "--include",            "*.html",
    "--include",            "*.xml",
    "--metadata-directive", "REPLACE",
    "--cache-control",      "max-age=0",
}));