ecko fmt

ecko fmt file.ecko            # format in place
ecko fmt src/ tests/          # several paths
ecko fmt --check file.ecko    # exit non-zero if it would change
ecko fmt --stdin < file.ecko  # print the formatted source (for editors)

A directory means every .ecko file under it. vendor/, target/, node_modules/ and dot-directories are skipped, the same as ecko test skips them: they hold other people's code or build output.

One canonical style, zero configuration. There are no options to argue about because there are no options.

Why it is not configurable

Formatting arguments are pure cost. More than that, Ecko treats the canonical form as part of the language contract: any new syntax must round-trip through the formatter byte-identically, and the compiler's own test suite formats every example twice and asserts the output is unchanged. A style knob would multiply that guarantee by the number of settings.

There is a second reason, particular to this language. Ecko is written to be read by models as well as people. One canonical form means a model sees the same shape for the same construct every time, in every codebase.

--check

Formats nothing, exits non-zero if any file is not already canonical, and names the files. This is the CI form:

ecko fmt --check .

--stdin

For editors and pre-commit hooks: reads the source from stdin and prints it formatted to stdout. With --check it prints nothing and answers by exit code, 0 if the source is already canonical and 1 if not. Source that does not parse is an error on stderr, exit 1, and nothing on stdout, so an editor never swaps a buffer for an error message.

What it does not change

Comments keep their text, and ## doc comments keep their marker. Blank lines between top-level definitions are preserved as paragraph structure - the formatter normalizes indentation, spacing, wrapping and delimiters, not your intent about what belongs together.

That extends to what a comment is attached to. A parameter list written across several lines with a comment per parameter stays broken, so each comment keeps its parameter:

fn area(
    width,  # in metres
    height,  # also in metres
) {
    width * height
}

Collapsing that signature would leave the comments nowhere to go but the body, where they would read as documentation of the first statement. A signature with no comments still collapses to one line, and a comment written after a single-line signature belongs to the statement rather than to the last parameter.

The same holds inside a statement. A comment on a match arm stays on that arm, one on a closing } stays on the }, a comment above an arm or at the end of a block stays inside it, and one after a function under an @attribute stays on the fn line:

@tool("search the docs")
fn docs(query) = search(query)  # our own retrieval

kind = match get(e, "kind") {
    "parse" => "bad file"  # treat as no settings
    # anything else is the caller's
    _ => "other"
}  # one arm per error kind

A comment at the end of a line that opens a block belongs to that block, so } else { # otherwise keeps its comment on the else.

A list, map or argument list with a comment inside it prints one item per line, and each comment stays where you wrote it: above the item it came before, after the item it followed, or after the opening bracket. Without the comments, the same literal collapses to one line when it fits:

types = {
    # text
    txt: "text/plain",
    md: "text/markdown",  # CommonMark
    # images
    png: "image/png",
}

A comment inside a lambda's body belongs to the body, so it leaves the call around it alone: test.case("name", fn() { ... }) keeps its shape.

Deprecated syntax

The formatter migrates syntax that has been superseded. const x = ... becomes let x = ..., the old lambdas |x| expr and || expr become fn(x) expr and fn() expr, Option<T> becomes T?, and a bare vote count ai[T] 5 "..." becomes ai[T] "..." votes 5. ecko check reports each old form as a deprecated-syntax warning pointing here. Running ecko fmt is the fix; to touch only the deprecated forms and leave the rest of a file's layout alone, use ecko fix --migrate.