Tidy Formatter — JS/CSS/HTMLSafe, modern beautifier for JS, CSS, HTML & JSON that respects your VS Code config — never auto-formats or hijacks your files unless you ask. Tidy is a configurable beautifier for JavaScript, TypeScript, JSX/TSX, CSS, SCSS, LESS, HTML and JSON. It does exactly one thing: it reformats a file when you ask it to — via Format Document or Format Selection — and it refuses to write a file back unless the result is provably equivalent to what you started with. It never registers itself as your default formatter, never hooks "save", and never touches a file you didn't tell it to. If you arrived here after a formatter quietly rewrote your code on save, that is the exact behaviour Tidy is built to make impossible. Migrating from JS-CSS-HTML FormatterJS-CSS-HTML Formatter ( Recommended migration (2 minutes), in this order:
Guided migration commandsBoth commands live in the Command Palette (
If lonefy is detected when Tidy activates, you may also see a single, non-modal notification offering to migrate. It is one-shot and deduplicated: it appears at most once per machine/profile, has a “Don’t ask again” action, and is never repeated — the incumbent’s #1 complaint was its intrusiveness, so Tidy will not nag. What gets imported from
|
.jsbeautifyrc key (lonefy) |
Tidy setting | Notes |
|---|---|---|
indent_size |
tidy.indent |
Integer in [1, 16]; out-of-range values are dropped with a note. |
brace_style |
tidy.brace_style |
One of collapse, expand, end-expand, none, collapse-preserve-inline. |
wrap_line_length |
tidy.wrap_line_length |
Non-negative integer; 0 disables wrapping. |
wrap_attributes |
tidy.wrap_attributes |
One of auto, force, force-aligned, force-expand-multiline, aligned-multiple, preserve, preserve-aligned. |
space_after_anon_function |
tidy.space_after_anon_function |
Boolean. |
.jsbeautifyrc import is a best-effort convenience, not a marketing promise —
most lonefy users ran it with no config at all. Tidy also reads your existing VS
Code settings (editor.tabSize, editor.insertSpaces, per-language overrides)
directly, so in most cases there is nothing to port over.
What lonefy breaks → what Tidy fixes
Every row below is sourced from a real Marketplace review or GitHub issue on the incumbent. Where a quote is shortened, the meaning is preserved.
| What JS-CSS-HTML Formatter does | What Tidy does instead | Source |
|---|---|---|
Formats on save even when you turned formatOnSave off — "Does not respect onSave: false." / "Always formats on save regardless of editor.formatOnSave". |
Tidy never hooks save at all. Format-on-save is owned entirely by VS Code; if editor.formatOnSave is off, nothing happens on save. |
Review "Does not respect onSave: false"; issue #143, #131 |
| Makes itself the default formatter and overrides other formatters — "Formats code when you save it, regardless of your default formatter." / "Prevents every other formatter from working." | Tidy never contributes or sets editor.defaultFormatter. If another formatter is your default, Tidy stays out of the way and works alongside it. |
Reviews "Completely broken… regardless of your default formatter", "Prevents every other formatter from working"; issue #92 |
Breaks JSX — "It formats <App /> to < App / >". |
Tidy formats JSX/TSX with a real parser (Prettier under the hood for those languages), and a safety guard rejects any output where a JSX tag boundary was mangled — even when the broken output still happens to re-parse. | Review "It formats <App /> to < App / >"; issues #64, #76 |
Corrupts modern operators — "Changing my valid ?. to ? . on save" and big-int literals like 1n turned into 1 n, producing syntax errors. |
These are preserved exactly. If any engine ever produced output that changed program meaning, the AST-equivalence guard discards it and leaves the file untouched. | Issue #146 ("?. → ? ."), #150; review about 1n → 1 n |
| Ignores your indent settings — "When saving using 2 spaces instead of tabs, it changes to 4 spaces." / "cannot set to 2 spaces". | Tidy honours editor.tabSize / editor.insertSpaces and the live FormattingOptions VS Code passes in, so 2-space projects stay 2-space with no config file. |
Issues #31, #100, #73 |
| Adds stray spaces in HTML attributes / class & id names — "every time I save the file it just randomly add space to some class name or id name." | The HTML guard re-tokenises and tree-compares output (parse5); any change that alters the tree is rejected, so attribute and identifier text cannot drift. | Reviews "randomly add space to some class name or id name", "adds whitespace after all of my ids and classes"; issues #106, #41 |
Breaks SCSS/CSS — "SASS interpolation breaks CSS calc() statement", and wraps mixins with +. |
CSS/SCSS/LESS output is verified against a PostCSS tree before it is applied; structurally different output is dropped rather than written. | Issue #74; review about calc(#{…}) interpolation |
| Reformats files you never want touched — "It keeps re-formatting my package.json". | Tidy only ever formats the document you explicitly invoke it on; it has no save/focus/file-watcher triggers, so nothing is reformatted in the background. | Review "keeps re-formatting my package.json"; issue #134 |
| Cross-file corruption — "rewrites another files after copy-paste" / "copying contents of files on file save". | Each format call returns edits for a single document only; there is no path by which one file's content can land in another. | Issues #56, #29, #102, #110 |
Before / after
These are real inputs from this repo's samples/ folder, run through Format Document. The point is not just that the output is tidy — it is that nothing in your code changed meaning along the way.
CSS — collapsed, comma-jammed rules become readable, and the combinator and calc() survive intact:
/* before — samples/messy.css */
.card{display:flex;padding:8px;color:red}
#main > .item:nth-child(2n){margin:calc(100% - 20px);background:blue}
a:hover{text-decoration:underline} .footer { gap : 12px }
/* after */
.card {
display: flex;
padding: 8px;
color: red
}
#main>.item:nth-child(2n) {
margin: calc(100% - 20px);
background: blue
}
a:hover {
text-decoration: underline
}
.footer {
gap: 12px
}
TSX — note what does not happen: <div className="card" … /> stays a valid JSX element (no < div / >), and n?.toString() ?? "none" keeps its optional-chaining and nullish-coalescing operators exactly:
// before — samples/messy.tsx
import {useState,useEffect} from "react"
type Props={title:string,count?:number}
export function Widget({title,count=0}:Props){
const [n,setN]=useState(count);useEffect(()=>{console.log(n?.toString()??"none")},[n])
return <div className="card" onClick={()=>setN(n+1)}><h1>{title}</h1><span>{n}</span></div>
}
// after
import { useState, useEffect } from "react";
type Props = { title: string; count?: number };
export function Widget({ title, count = 0 }: Props) {
const [n, setN] = useState(count);
useEffect(() => {
console.log(n?.toString() ?? "none");
}, [n]);
return (
<div className="card" onClick={() => setN(n + 1)}>
<h1>{title}</h1>
<span>{n}</span>
</div>
);
}
If either output had altered the parse tree — a mangled JSX tag, a ?. split into ? . — the equivalence guard would have discarded it and left your file untouched (see Safety guarantees below).
Safety guarantees
Tidy's core promise: we will never write a file that fails an equivalence check against your original. Concretely:
- AST-equivalence guard (JS / TS / JSX / TSX). After formatting, Tidy parses
both the input and the output and compares them modulo whitespace and style.
If they are not semantically equivalent, Tidy returns zero edits and your
file is left exactly as it was. This includes a dedicated JSX tag-boundary
check, so a mangled
< Foo bar = {x} />is rejected even though it still re-parses as valid TSX — a parse-only check would let that corruption through; Tidy does not. - Re-tokenise + tree compare (CSS / SCSS / LESS / HTML). For languages without a strict parse-or-fail AST, Tidy compares a PostCSS tree (CSS family) or a parse5 tree (HTML). Any output whose tree differs from the input's is discarded.
- Value-level check (JSON / JSONC). The parsed value must be unchanged.
- Idempotence.
format(format(x))equalsformat(x), so your code does not drift to the right every time you reformat. - Single-document isolation. A format call only ever produces edits for the document you invoked it on. No background watchers, no cross-pane copying.
- No silent failures. When the guard aborts, you get a non-blocking notice and a line in the Tidy Formatter output channel explaining why. That detail never contains your source code — only the language, engine, and a short reason.
If the guard ever aborts a format you expected to succeed, that is by design: the file is intact, and the alternative (writing potentially corrupted output) is exactly the failure mode Tidy exists to prevent.
60-second opt-in setup
Tidy does nothing on save until you opt in. That is deliberate — it is the opposite of the incumbent's "it formats whether you like it or not". Two independent choices:
1. Format manually (no setup needed).
Open any supported file and run Format Document (Shift+Alt+F) or select code and run Format Selection. If Tidy is the only formatter for that language it just works; otherwise pick it once via Format Document With… → Tidy Formatter.
2. Make Tidy your default formatter (optional).
Right-click in an editor → Format Document With… → Configure Default Formatter… → choose Tidy Formatter — JS/CSS/HTML. Or in settings.json, per language:
{
// Example: use Tidy for CSS, keep Prettier for everything else.
"[css]": { "editor.defaultFormatter": "ced-lab.tidy-formatter" }
}
3. Turn on format-on-save yourself (optional). This is a stock VS Code setting that Tidy never sets for you:
{
"editor.formatOnSave": true
}
You can scope it per language so Tidy only runs on the languages you want:
{
"[css][scss][less]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "ced-lab.tidy-formatter"
}
}
That's it — there is no Tidy-specific "format on save" toggle, because Tidy delegates the entire save lifecycle to VS Code.
"It does nothing?" If you installed Tidy and saving no longer reformats, that is expected: Tidy never auto-formats. Complete steps 2 and 3 above to choose the behaviour you want.
Supported languages
| Language | Engine | Safety guard |
|---|---|---|
| CSS, SCSS, LESS | js-beautify | PostCSS tree compare |
| HTML | js-beautify | parse5 tree compare + template-island preservation |
| JSON, JSONC | js-beautify | parsed-value compare |
| JavaScript | js-beautify (real parser if JSX is detected) | AST equivalence (@babel/parser) |
| TypeScript | real parser (Prettier) | AST equivalence |
| TSX (TypeScript React) | real parser (Prettier) | AST equivalence |
| JSX (JavaScript React) | real parser (Prettier) | AST equivalence |
Plain .js files are formatted with js-beautify for familiarity, but if Tidy detects JSX inside them it automatically re-routes to the real parser rather than emit broken output.
Tidy also reads your project's .editorconfig cascade (honouring root = true) and maps indent_style, indent_size / tab_width, end_of_line, insert_final_newline, trim_trailing_whitespace and max_line_length. A team .editorconfig therefore wins over plain VS Code settings. Opt out with "tidy.editorconfig": false. In Restricted Mode the workspace .editorconfig is ignored (it is workspace-authored content), falling back to built-in and user settings.
.sass (indented syntax) and Vue/Astro single-file components are not in this build; see the spec for the v1.0/v2 roadmap.
Works alongside Prettier
Tidy is a configurable beautifier, not an opinionated replacement for Prettier — and it is built to sit next to it, not on top of it.
- Tidy never declares itself your default formatter. It does not contribute or write
editor.defaultFormatter, so it never silently takes over a language Prettier already owns. (The incumbent did the opposite — see issue #92, "prevents every other formatter from working".) - You assign each language yourself. A common setup is Prettier for JS/TS and Tidy for CSS/HTML/JSON. Because nothing competes for the default slot, that split is just two per-language
editor.defaultFormatterentries — both formatters stay installed and active, each on the languages you chose. - For TS, JSX and TSX, Tidy is Prettier under the hood. Those languages are formatted by the same Prettier engine, then checked by Tidy's AST-equivalence guard. So even when Tidy is the formatter you invoked, the actual reflow on your React/TypeScript code is Prettier's — there is no second, conflicting style to reconcile.
In short: keep Prettier exactly as it is. Tidy only ever runs on the languages you explicitly hand it, and it gets out of the way everywhere else.
Ignore & coexistence
Tidy gives you fine-grained control over what it touches and gets out of the way when another formatter already owns the project — three things the abandoned incumbent never did. None of this can corrupt a file: every protected span is restored verbatim, so the equivalence guard sees output equal to the input and accepts it; if a splice ever produced non-parsable output the guard rejects it and your file is left intact.
| Capability | lonefy (#16, abandoned 2017) | Tidy |
|---|---|---|
| Skip a region / node in-source | ✗ none (#16) | tidy-ignore-start/end, // tidy-ignore, // prettier-ignore |
| Exclude files project-wide | ✗ none | .soukformatignore (gitignore syntax) |
| Step back for Prettier/Biome/dprint | ✗ hijacks the default slot (#92) | one-shot deference notice; never changes editor.defaultFormatter |
In-source ignore directives
Write a comment (in the host language's syntax) to keep part of a file exactly as authored:
/* tidy-ignore-start */
.keep { color : red ; } /* this block is preserved BYTE-for-BYTE */
/* tidy-ignore-end */
.rest { color: blue; } /* …while everything else is reformatted */
- Whole file — a head comment containing
tidy-ignore-file,tidy-ignore, orprettier-ignore(the first significant comment at the top) leaves the entire document untouched. - A region —
tidy-ignore-start…tidy-ignore-endpreserves everything in between, verbatim (markers included). An unterminated region protects to end-of-file (conservative — it protects more, never less). - The next node — a lone
// tidy-ignoreor// prettier-ignorecomment protects the node on the following line (best-effort, conservative line heuristic).
Engine coverage. For TS/TSX/JSX (the Prettier path) // prettier-ignore is honoured natively at the node level by Prettier itself — the richer, syntax-aware behaviour. For CSS/SCSS/LESS/HTML/JSON and plain JS (the js-beautify path) Tidy protects tidy-ignore-* regions itself via a mask-and-restore step behind the guard. Node-level masking on the js-beautify path is best-effort: if a coarse span can't be spliced back safely, the guard simply rejects and the file stays intact (never corruption). Node-level directives are richest on the Prettier path; this js-beautify limitation is documented rather than risked.
.soukformatignore
Drop a .soukformatignore at your project root (or any sub-folder) to exclude files from formatting entirely — Tidy leaves them byte-identical. It uses familiar gitignore syntax:
# vendored / generated — never reformat
*.min.css
dist/
vendor/**/*.js
# …but DO keep this one tidy (negation re-includes it)
!vendor/keep.css
- Last matching pattern wins; a trailing
!negationre-includes a previously-excluded path (git semantics). - The cascade walks up from the file: a root
.soukformatignoreand a sub-folder one both apply. - It is read only in trusted workspaces (ignored in Restricted Mode → the file is formatted). If the ignore file can't be read, Tidy fails safe and formats normally.
- Turn it off entirely with
"tidy.respectSoukformatignore": false.
Preview command (read-only diff + atomic undo)
Run Tidy: Preview Format (diff) (tidy.previewFormat) from the Command Palette to see exactly what Tidy would change before committing to it:
- Opens a read-only side-by-side diff (original vs. formatted). Opening it writes nothing — your file's dirty state is unchanged.
- The preview runs the same pipeline as a normal format, including the equivalence guard. If the guard would reject the output, no diff opens and you get a non-blocking notice (file intact).
- Click Apply to write the result as a single undo entry — one
Ctrl+Zfully reverts it. Dismiss the prompt and nothing is written.
Deference behavior (coexisting with other formatters)
When a workspace already configures another formatter — Prettier (.prettierrc* or a prettier key in package.json), Biome (biome.json), or dprint (dprint.json) — Tidy surfaces that fact once, and otherwise stays out of the way. Controlled by tidy.deferToOtherFormatters (default notify):
| Value | Behavior |
|---|---|
notify (default) |
Show a one-time, informational notification per workspace that another formatter is configured. Deduplicated via globalState — never a repeat nag. |
silent-defer |
Acknowledge the competitor without notifying. Still writes nothing and disables nothing. |
off |
Ignore detection entirely (no read, no notification). |
The anti-hijack contract (non-negotiable): deference never changes editor.defaultFormatter, never disables Tidy silently, and never writes any setting on its own. The notification is purely informational — it reminds you that you can disable Tidy per language (tidy.<lang>.enable) for this workspace if you prefer the other tool to own formatting. The choice, and any write, is always yours. A .prettierignore on its own does not trigger deference (it only narrows what Prettier would touch, it doesn't configure Prettier as a formatter). Detection reads workspace files only in trusted workspaces.
Tip: run Tidy: Show Effective Configuration to see, for the active document, whether it would be skipped (
.soukformatignore/ in-source marker), how many regions are protected, and which competing formatters were detected.
Key settings
All settings live under the tidy.* namespace. Indentation defaults come from your editor (editor.tabSize / editor.insertSpaces); the tidy.* values below are fallbacks used only when neither the editor nor a project config provides one.
| Setting | Default | What it does |
|---|---|---|
tidy.<lang>.enable |
true |
Per-language switch (tidy.css.enable, tidy.typescript.enable, …). Set to false to remove Tidy as a formatter for that language. |
tidy.indent |
4 |
Fallback indent size (js-beautify indent_size) when the editor provides none. |
tidy.brace_style |
collapse |
Brace placement (collapse, expand, end-expand, none, collapse-preserve-inline). |
tidy.wrap_line_length |
0 |
Max characters per line before wrapping; 0 disables wrapping. |
tidy.wrap_attributes |
auto |
How HTML attributes wrap (auto, force, force-aligned, force-expand-multiline, aligned-multiple, preserve, preserve-aligned). |
tidy.space_after_anon_function |
false |
Add a space after an anonymous function keyword. |
tidy.maxFileSizeKB |
5120 |
Skip (with a notice) documents larger than this, so huge files never freeze the editor. 0 disables the guard. |
tidy.editorconfig |
true |
Read the project's .editorconfig cascade and let it override plain VS Code settings. Set to false to ignore .editorconfig. |
tidy.soukformatrc |
true |
Read the project's .soukformatrc file (JSONC) for per-language options and glob overrides, layered above .editorconfig. Set to false to ignore it. Always ignored in Restricted Mode. |
tidy.respectSoukformatignore |
true |
Skip files matched by a project .soukformatignore (gitignore syntax), leaving them byte-identical. Set to false to format every file regardless. Always ignored in Restricted Mode. See Ignore & coexistence. |
tidy.deferToOtherFormatters |
notify |
How Tidy reacts when another formatter (Prettier/Biome/dprint) is already configured: notify (one-time notice), silent-defer (no notice), or off. Never changes your default formatter. See Deference behavior. |
The full option set ("not enough options" — fixed). Beyond the headline knobs above, Tidy exposes the complete js-beautify surface by family and the AST-safe Prettier stylistic options for TS/JSX. A few highlights:
| Setting | Applies to | What it does |
|---|---|---|
tidy.preserve_newlines / tidy.max_preserve_newlines |
all | Keep existing blank lines (and cap how many). |
tidy.space_in_paren, tidy.break_chained_methods, tidy.keep_array_indentation, tidy.comma_first, tidy.operator_position |
JavaScript | Fine-grained js-beautify JS layout. |
tidy.selector_separator_newline, tidy.newline_between_rules, tidy.space_around_combinator |
CSS/SCSS/LESS | CSS rule + selector spacing. |
tidy.indent_inner_html, tidy.indent_scripts, tidy.wrap_attributes_indent_size |
HTML | HTML structure + attribute wrapping. |
tidy.prettier.singleQuote, tidy.prettier.semi, tidy.prettier.jsxSingleQuote, tidy.prettier.trailingComma, tidy.prettier.bracketSpacing, tidy.prettier.bracketSameLine, tidy.prettier.arrowParens, tidy.prettier.printWidth |
TS/TSX/JSX/JS | Stylistic only — quote style, semicolons, trailing commas, parens, line width. Each is AST-invariant, so the equivalence guard still accepts the result. |
Per-language overrides. Every option above is language-overridable, so you can scope it to one language in your settings.json:
{
"tidy.brace_style": "collapse",
"[javascript]": { "tidy.brace_style": "expand" },
"[typescript]": { "tidy.prettier.singleQuote": true }
}
Project config — .soukformatrc (JSONC). Drop a .soukformatrc at your project root to drive style per language (and per glob), shared across the team and layered above .editorconfig:
{
// per-language sections (omit the "tidy." prefix)
"css": { "indent": 2, "newline_between_rules": false },
"typescript": { "singleQuote": true, "semi": false },
// optional ordered glob overrides (most specific wins)
"overrides": [
{ "files": "src/**/*.css", "options": { "indent": 8 } }
]
}
A malformed .soukformatrc never breaks formatting — it is ignored with a warning and Tidy falls back to the lower layers. It is read only in trusted workspaces.
Diagnosing your config: run Tidy: Show Effective Configuration from the Command Palette to see every resolved option and the exact source of each value (VS Code global, tidy.<lang>, .editorconfig, .soukformatrc, or a glob override). This is the direct answer to "why is it indenting with 4 spaces?" — no guesswork, no hours of debugging.
Commands
All Tidy commands are in the Command Palette (Ctrl+Shift+P) under the Tidy category:
| Command id | Title | What it does |
|---|---|---|
tidy.showEffectiveConfiguration |
Tidy: Show Effective Configuration | Shows every resolved option and the exact source of each value. |
tidy.useAsFormatter |
Tidy: Use Tidy as my Formatter | Opt in per language: writes editor.defaultFormatter (Workspace by default) for the languages you pick. Never writes editor.formatOnSave; cancelling writes nothing. |
tidy.runMigration |
Tidy: Migrate from JS-CSS-HTML Formatter | Imports a legacy .jsbeautifyrc into tidy.* settings after a confirmation recap (trusted workspaces only), then optionally runs Use Tidy as my Formatter. |
tidy.previewFormat |
Tidy: Preview Format (diff) | Opens a read-only diff of what Tidy would change, then applies it on an explicit Apply click as a single undo entry. Opening the diff writes nothing. See Preview command. |
FAQ
Will Tidy format my files on save?
Only if you enable editor.formatOnSave and select Tidy as the default formatter for that language. Out of the box it does nothing on save.
Will it override Prettier / ESLint / my chosen formatter?
No. Tidy never sets or contributes editor.defaultFormatter. If another formatter is your default, Tidy does not run unless you explicitly invoke it.
I installed it and nothing happens when I save. Is it broken? No — that is the intended default. See 60-second opt-in setup to choose the behaviour you want.
Can it corrupt my code? Tidy refuses to apply any output that fails its equivalence guard (AST for JS/TS/JSX/TSX, tree compare for CSS/HTML, value compare for JSON). If formatting would change meaning, you get zero edits and an intact file plus a notice.
Does it support JSX and TypeScript? Yes — JSX, TSX, and TypeScript are formatted with a real parser, not a token mangler, and are covered by the AST guard.
Does it send my code anywhere? No. Tidy is 100% client-side. It does not phone home, and the output-channel diagnostics never include your source.
Does it work in Restricted Mode / virtual workspaces / Codespaces? It declares support for untrusted and virtual workspaces. In Restricted Mode it falls back to built-in and user settings rather than workspace-defined config.
How do I stop Tidy from formatting a specific language?
Set tidy.<lang>.enable to false (e.g. "tidy.json.enable": false), or simply don't choose it as the default formatter for that language.
How do I stop Tidy from touching a specific file or region?
Three ways, all covered in Ignore & coexistence: a .soukformatignore (gitignore syntax) excludes files project-wide; tidy-ignore-start/tidy-ignore-end comments preserve a region byte-for-byte; and a head tidy-ignore-file / // prettier-ignore comment skips the whole file.
Another formatter (Prettier/Biome) is already set up — will Tidy fight it?
No. Tidy detects it and shows a one-time, informational notice (tidy.deferToOtherFormatters, default notify). It never changes editor.defaultFormatter and never disables itself silently — see Deference behavior.
Can I preview a format before applying it? Yes — run Tidy: Preview Format (diff) for a read-only side-by-side diff, then Apply it as a single undo entry. See Preview command.
Does it read .editorconfig?
Yes. Tidy reads the .editorconfig cascade (respecting root = true) and maps the common keys (indent_style, indent_size/tab_width, end_of_line, insert_final_newline, trim_trailing_whitespace, max_line_length). Disable with "tidy.editorconfig": false.
Can I configure it with a project file?
Yes. Tidy reads a .soukformatrc (JSONC) from your project root — per-language sections plus ordered glob overrides, layered above .editorconfig and shared across the team. See Key settings for the schema. (YAML support is on the roadmap.)
Where's the indented-.sass / Vue support?
Not in this build. The remaining roadmap (.soukformatrc YAML, real CSS-in-JS reformatting, Vue SFCs, Open VSX, more languages) is described in SPEC.md. Ignore directives, .soukformatignore, the preview diff, and deference notifications are shipped — see Ignore & coexistence.
Feedback
Found a bug or a file Tidy got wrong? A reproducible input is worth a thousand stars — open an issue with the smallest snippet that reproduces it. If Tidy has saved you from a save-on-format disaster, you're welcome to leave a review. No in-product nagging, ever.
License
MIT. See CHANGELOG.md for release history.