Skip to content

feat: add useTabs config option - #253

Open
dsherret wants to merge 3 commits into
mainfrom
feat_use_tabs
Open

feat: add useTabs config option#253
dsherret wants to merge 3 commits into
mainfrom
feat_use_tabs

Conversation

@dsherret

@dsherret dsherret commented Aug 24, 2026

Copy link
Copy Markdown
Member

Partially addresses #242.

Writes the four columns of indentation of an indented code block as a tab:

Test:

	code

Same mechanism as prettier/prettier#19352, minus the footnote half — see below.

Scope

useTabs is not inherited from dprint's global configuration; it only takes effect when set in the markdown plugin's own section. A project that indents its code with tabs isn't saying anything about the few places markdown can use one, and inheriting it changed the output of ~2.8% of the markdown files on hand with no config change on the user's part. (This is also why the PR only partially addresses #242 — the reporter asked for lists too.)

CommonMark fixes tab stops at four columns, so a tab only stands for those four where it is written at a column that is already a multiple of four. Everything else keeps its spaces:

  • a block a list indents off a tab stop — - one puts its content at column 2, where a tab reaches column 4 and gives two columns rather than four;
  • anything within a block quote, whose markers are written into the item stream rather than as indentation, so the column its content begins at isn't known where the decision is made;
  • a block that begins partway along a line, beside a list marker or a [^1]: / : label.

Lists are unchanged, and so is every other kind of indentation. Note that under list.indentKind: pythonMarkdown a list's own increment is a fixed 4, so a code block inside a list does reach a tab stop and gets a tab (- one / \tcode) — pinned in CodeBlocks_UseTabs_PythonMarkdown.txt.

Why not footnote definition bodies

An earlier revision covered them, as prettier's PR does. Review turned up a limitation with no fix on this side: dprint-core's writer counts a tab as indent_width columns — 1 here, since the plugin sets indent_width: 1 so that column-granular indents are expressible at all — so a tab-indented footnote body wrapped about three columns wide and overran lineWidth under textWrap: always. Code block content is never wrapped, so the code block half is unaffected.

That half also needed a print-item walker to insert the tab into arbitrary generated content, plus plumbing so a block quote inside a footnote wrote the tab ahead of its > markers. Dropping it removes all of that: gen_block_quote and gen_block_quote_markers are untouched again, and the whole feature is one predicate and one line-writing helper.

Lists would need the same dprint-core change, and more besides: the column a list item's content begins at follows the width of its marker, so putting it on a tab stop means writing the marker with a tab after it and choosing different content columns. Separate work.

Testing

tests/use_tabs_test.rs is an oracle over the whole spec corpus. Every spec is formatted with useTabs added to whatever configuration it already sets, and the result written back out with the option off, which must equal what the formatter produces from spaces alone — a tab that failed to reach its column would be read back as something else and show up there. It also pins idempotency under tabs and checks no line gains trailing whitespace.

Verified beyond the specs:

  • No change for anyone not setting the option. main's binary against this branch over 160,505 formatted outputs — 12,101 documents including 10,552 real .md files, plus a 20,000-document nesting fuzz corpus, across five configs — zero differing bytes. Turning the option on changes 358 of those same documents, which is what shows the comparison was live.
  • 4,905 real markdown files under useTabs: 55 reformat, all idempotent, all oracle-clean.
  • The guards are mutation-tested: dropping the tab-stop check, the block quote check, or the start-of-line check each fails a pinned spec.

Known limitation

Where the first pass also re-indents the document — list renumbering under pythonMarkdown, task-list-marker normalisation — the parse shifts underneath the indent_level % 4 decision and a second pass can rewrite the same columns as spaces instead of a tab. It converges after that. Fuzzing found two such inputs in ~40k random documents and none in the 4,905 real files; space mode converges in one pass for both.

Writes the four columns of indentation that an indented code block and
the body of a footnote definition are written with as a tab.

A tab reaches to the next multiple of four columns, so it only stands
for those four where the content around the block begins at a multiple
of four itself. Either keeps its spaces where it doesn't, and within a
block quote, whose markers leave the column its content begins at
unknown. A list keeps its spaces throughout, since the column its
content begins at follows the width of its marker.

Closes #242
Review found the footnote definition half was the source of most of the
change's complexity and of a limitation with no fix here: the printer
measures a tab as one column, so a footnote body wrapped ~3 columns wide
and overran the line width. Code blocks are unaffected, since their
content is never wrapped, so this drops the footnote half along with the
machinery it needed -- `gen_block_quote` and its markers are untouched
again.

Also fixes an indented code block written beside a list marker being
given a tab, which reached the next tab stop from the marker's column
rather than standing for four columns and left the code read back as a
paragraph. A tab now only goes where the block begins a line of its own.

The option is no longer taken from the global configuration. A project
that indents its code with tabs isn't saying anything about the few
places markdown can use one, and inheriting it changed the output of
~2.8% of the markdown files on hand without any config change.

The oracle test was comparing a spec that sets `useTabs` itself against
a configuration that also set it, so it was inert on exactly the specs
that exercise the feature.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant