Isaac Parker

Markdown-formatted comments

Code formatters encode a simple principle: source should have a consistent, deterministic style. They eliminate the manual effort of adjusting whitespace and reduce the cognitive burden of reading code written in many subtly different styles. But comments, despite being part of that same maintained source, are usually treated as opaque text, with formatting left as an afterthought.

A large portion of the (relatively minuscule) amount of time I spend typing into an editor now is writing comments. Writing good comments that capture intent, context, or an architectural model is an editorial task involving judgment that I have yet to see coding agents handle consistently, even as their ability to write code improves. Those comments can be critical to the long-term maintainability of a codebase.

What should not require judgment is arranging that prose on the page. I still spend time manually wrapping and reflowing comments to the same line width as the surrounding code, and when agents rewrite comments, they likewise either leave jagged wrapping or spend tokens and iteration cycles manually reflowing prose. That mechanical work should simply be automatic.

Some tools do format comments, but typically as a narrow, opt-in capability rather than a baseline formatter guarantee:

As I’ve been working on a new language and its unified compiler toolchain, I wanted first-class, always-on support for deterministic Markdown formatting of comments.

Language design

The language originally had three classes of comments:

I wanted the first two to support full Markdown formatting: prose would wrap to the same fixed line width used for formatting code, while Markdown elements such as headers, bulleted lists, and code blocks would be supported directly.

There is, however, a legitimate need for opaque comments. ASCII diagrams and commented-out code are the most obvious examples: their existing layout clearly should not be changed by the formatter.

To account for this, I introduced a new type of comment: an “opaque line comment”, beginning with //|, that explicitly opts out of any wrapping or formatting, while remaining visually unobtrusive. Editor shortcuts to comment out existing code (often cmd + /) will use opaque line comments, while beginning a new comment is assumed to be prose and will use //.

The formatting pipeline

For background, source is formatted by:

Our Layout model and printing algorithm are based on Philip Wadler’s A prettier printer and the strict formulation presented by Christian Lindig in Strictly Pretty.

S o u r c e t e x t p a r s e a n d c a n o n i c a l i z e C a ( n l F o a o n n r i b L g m c u a u a a i y a t l l o g p t d u e r e s t - i d y l a n n a t g t s t y r n o a o e o u x u e s r t t c t i e r c e ) e

An important invariant: the Layout data model and the print algorithm are language-agnostic. This is valuable decoupling: as we evolve the language, adding new syntax features and refactoring the AST data model, the print algorithm remains unaffected.

Our Layout model is a recursive enum describing a tree of pretty-printing operations:

enum Layout {
    Empty,
    Text(String),
    Newline,
    SpaceOrNewline,
    NewlineIfBroken,
    BreakEnclosingGroup,
    Sequence(Vec<Self>),
    Indent(Box<Self>),
    Group(Box<Self>),
    BrokenOrFlat { broken: Box<Self>, flat: Box<Self> },
    OpaqueText(String),
}

Most nodes are self-evident: Empty prints nothing, Text prints text, BrokenOrFlat describes two alternate layouts which the printing algorithm can choose between based on available space, etc.

One extension to the standard model was OpaqueText: subtly different from the standard Text, the printer emits its contents, which may contain newlines, without treating those newlines as layout operations. This, along with a lossless syntax tree, allows our formatter to operate on invalid or incomplete code: regions that cannot be understood by the parser are preserved byte-for-byte while everything else is printed from its parsed tree.

Implementing Markdown formatting

I first attempted a minimal prototype that only supported prose wrapping: it used a new Fill layout, which would fill as many items within the available space before wrapping them. To avoid wrapping non-prose structures such as bulleted lists and code fences, the layout builder used rough heuristics to identify Markdown elements: basically whether a line started with #, -, or another recognized marker.

This worked, and validated the user experience of automatically wrapping comment prose. But it relied on crude heuristics, and didn’t actually support formatting Markdown elements.

Fully featured Markdown formatting requires a real Markdown formatter. Implementing one within the compiler would mean owning substantial parsing and formatting complexity, along with its edge cases and maintenance burden. Reusing an existing formatter was the clear choice.

The codebase already used dprint-plugin-markdown to format Markdown files, and I was quite happy with it: it’s well-maintained, comprehensive, and handles edge cases well. But now we needed to integrate this formatter into our compiler’s printer engine. This might sound trivial at first, until indentation and available width enter the picture.

I eventually came up with a clean design that preserves the language-agnostic Layout data model, in the form of a “self-formatting block” layout node using a Rust trait:

trait SelfFormattingBlock {
    fn format(&self, available_width: usize) -> Vec<String>;
}

enum Layout {
    // ... existing layout nodes
    SelfFormattingBlock(Box<dyn SelfFormattingBlock>),
}

Our language-aware layout builder can then define an implementation of that trait for Markdown comments:

struct MarkdownCommentBlock {
    source: String,
    line_prefix: &'static str,
}

impl SelfFormattingBlock for MarkdownCommentBlock {
    fn format(&self, available_width: usize) -> Vec<String> {
        // Call dprint-plugin-markdown's public formatting api,
        // using the available width.
    }
}

The build step simply turns a comment AST node into:

Layout::SelfFormattingBlock(Box::new(MarkdownCommentBlock {
    source,
    line_prefix,
}))

while the printer operates on an opaque “self-formatting block”, providing it with the available width at print time.

This keeps each part of the pipeline focused on what it knows. The language-aware builder identifies the comment as Markdown and preserves its source and line prefix without needing to know its eventual rendered width. At print time, the language-agnostic printer supplies that width without knowing anything about Markdown. The block itself delegates the formatting to an existing, comprehensive formatter.

The result is that standalone prose comments receive the same deterministic treatment as the surrounding code, while //| provides an explicit escape hatch for content whose layout must remain untouched.