badness 0.12.0

A language server, formatter, and linter for LaTeX
# Configuration

badness is configured through a `badness.toml` file. All keys are optional and
spelled in kebab-case; an unknown key or section is a hard error, not a silent
no-op. Run `badness init` to write a commented starter file showing every key at
its default.

```toml
# Gitignore-style patterns to skip during directory discovery.
# exclude = [".git/"]
# extend-exclude = []

[format]
# line-width = 80
# indent-width = 2
# wrap = "reflow"  # reflow | stable | sentence | semantic | preserve

[lint]
# select = ["..."]  # if set, only these rules run
# ignore = []       # rules to disable
```

## Discovery

For each input, badness walks from the file's directory upward and uses the
first `badness.toml` it finds. The walk stops at a directory containing a `.git`
entry (the repository root), so a config outside your repository is never picked
up.

If no project file is found, badness next checks the `BADNESS_CONFIG`
environment variable. When set (and non-empty), it names a config file to use
instead of the global user config below—handy for keeping one config on a synced
drive and pointing every machine at it. A set `BADNESS_CONFIG` shadows the
global config entirely, and a missing or malformed file is a hard error rather
than a silent fall-through, so a typo'd path can't go unnoticed.

If `BADNESS_CONFIG` is unset, badness falls back to a global user config: the
first existing file among

1. `$XDG_CONFIG_HOME/badness/config.toml`
2. `~/.config/badness/config.toml`
3. the platform config directory (`%APPDATA%\badness\config.toml` on Windows,
   `~/Library/Application Support/badness/config.toml` on macOS)

The `BADNESS_CONFIG` and global files use the same schema as a project
`badness.toml` and are whole-file fallbacks, never merged with a project config.
Relative `exclude` patterns in them resolve against the working directory (CLI)
or the document's directory (language server) rather than the config's own
directory. The language server uses the same resolution, so both are easy ways
to set editor-wide defaults such as `wrap = "preserve"` (an edit is picked up
when the server restarts). If none of these files is found, built-in defaults
apply.

Two global CLI flags override discovery:

- `--config <PATH>` uses that file instead of discovering one.
- `--no-config` ignores any project, `BADNESS_CONFIG`, or global file and uses
  built-in defaults.

CLI flags for individual options (`--line-width`, `--wrap`, `--select`, …)
override the corresponding config values for a single run.

## Top level

### `exclude`

Gitignore-style patterns to exclude from directory discovery, resolved relative
to the directory containing the `badness.toml`. Excludes apply to both `format`
and `lint`, which share one file walk, so this is a top-level key rather than a
`[format]` option.

When set, this **replaces** the built-in default set (`[".git/"]`); use
[`extend-exclude`](#extend-exclude) to add patterns without restating the
defaults. Patterns given with the `--exclude` CLI flag are always added on top.

**Default value**: `[".git/"]`

**Type**: array of strings

**Example**:

```toml
exclude = ["vendor/", "old-drafts/"]
```

### `extend-exclude`

Gitignore-style patterns added *in addition to* the base set selected by
[`exclude`](#exclude) (the built-in defaults when `exclude` is unset). Use this
to skip a few extra paths without replacing the defaults.

**Default value**: `[]`

**Type**: array of strings

**Example**:

```toml
extend-exclude = ["build/"]
```

## `[format]`

Options for `badness format`. Each mirrors a CLI flag of the same name, which
takes precedence for a single run.

### `line-width`

Maximum line width before the formatter breaks a line. Must be between 1 and 1000.

**Default value**: `80`

**Type**: integer

**Example**:

```toml
[format]
line-width = 100
```

### `indent-width`

Spaces per indent step. Must be between 1 and 1000.

**Default value**: `2`

**Type**: integer

**Example**:

```toml
[format]
indent-width = 4
```

### `wrap`

How the formatter lays out line breaks *inside a paragraph*. It does not affect
structure—only where soft line breaks fall.

  | Mode       | Behavior                                                                                                        |
  | ---------- | --------------------------------------------------------------------------------------------------------------- |
  | `reflow`   | Greedy fill: pack words up to `line-width`, breaking only where the next word would overflow.                   |
  | `stable`   | Preserve acceptable authored breaks and rebalance only text that no longer fits (keeps revision diffs small).   |
  | `preserve` | Leave the authored line breaks untouched.                                                                       |
  | `sentence` | One sentence per line. Line width is ignored—a long sentence stays on one line.                                 |
  | `semantic` | [Semantic line breaks]https://sembr.org: keep the author's soft breaks *and* add a break after each sentence. |

Both `sentence` and `semantic` split a paragraph at sentence boundaries, one
sentence per line. Boundary detection is a small per-language rule engine over
the words: a `.`, `!`, or `?` ends a sentence *unless* the word is a known
abbreviation (`e.g.`, `Fig.`, `Dr.`, …), an ellipsis (`...`, `…`), or a
contextual abbreviation whose following word signals that the sentence continues
(`U.S. Government` stays together, `U.S. However` splits). The abbreviation
profile is chosen by [`lang`](#lang) and extended by
[`no-break-abbreviations`](#no-break-abbreviations).

`semantic` additionally **preserves the author's own line breaks** on top of the
sentence breaks (the [sembr](https://sembr.org) convention). It does not detect
clause boundaries itself—a break after a comma or `and` survives only where the
author placed a newline. A run-on sentence on a single source line is still
sentence-split.

`stable` also preserves authored line breaks, but treats them as preferred
anchors rather than hard boundaries. It is aimed at keeping revision diffs
small: a small prose edit perturbs the smallest possible region. Each prose run
is solved as one global layout problem. Candidate layouts are compared
lexicographically by total overflow, underflow below a soft target
(`line-width - 15`), changed authored breaks, displacement from the nearest
authored break, raggedness around that target, and line count. This makes the
hard width non-negotiable before minimizing source churn, while a short final
line remains unpenalized. Blank lines and command-only lines bound each
independently optimized run, and code-like statement bodies retain ordinary
greedy fill. (The soft target is not currently configurable.)

When omitted, the formatter uses each file kind's default: `.tex` and `.bib`
files reflow, while code-heavy `.sty`, `.cls`, `.dtx`, and `.ins` files preserve
authored line breaks. Setting `wrap` applies the same mode to every file kind.

**Default value**: unset (per file kind: `.tex`/`.bib` → `reflow`,
`.sty`/`.cls`/`.dtx`/`.ins` → `preserve`)

**Type**: `"reflow" | "stable" | "sentence" | "semantic" | "preserve"`

**Example**:

```toml
[format]
wrap = "stable"
```

### `math-wrap`

How the formatter lays out line breaks inside *display math*: `\[…\]`, `$$…$$`,
and single-formula math environments such as `equation`. Alignment-grid
environments (`align`, `gather`, matrices) and inline `$…$` math are not
affected.

  | Mode          | Behavior                                                                                                              |
  | ------------- | --------------------------------------------------------------------------------------------------------------------- |
  | `auto`        | Derive from the effective [`wrap`]#wrap: `preserve` keeps authored math breaks, every other mode breaks (amsmath).  |
  | `preserve`    | Keep the authored line breaks inside the body. Spacing within each line is still normalized.                          |
  | `single-line` | Never insert breaks: the body stays on one line, overflowing `line-width` if too long (like inline math).             |
  | `break`       | Break a too-long body before its top-level relations and binary operators, aligning a relation chain (amsmath style). |

**Default value**: `"auto"`

**Type**: `"auto" | "preserve" | "single-line" | "break"`

**Example**:

```toml
[format]
math-wrap = "preserve"
```

### `lang`

Document language as a BCP-47-style code (`en`, `de`, `pt-BR`, …), used by the
`sentence` and `semantic` wrap modes to pick the sentence-boundary abbreviation
profile. Built-in profiles cover English (default), Czech, German, Spanish, and
French; the region subtag is folded away, and an unknown or unset language falls
back to English. (Automatic detection from `babel`/`polyglossia` is not yet
implemented.)

**Default value**: unset (English)

**Type**: string

**Example**:

```toml
[format]
lang = "de"
```

### `no-break-abbreviations`

User-supplied no-break abbreviations for the `sentence` and `semantic` wrap
modes, keyed by language code or the literal `default` bucket (applied to every
document). An abbreviation listed here never ends a sentence, so no line break
is inserted after it. Merged on top of the built-in per-language lists.

**Default value**: `{}`

**Type**: table of string arrays, keyed by language code or `default`

**Example**:

```toml
[format.no-break-abbreviations]
default = ["ibid."]         # applied to every document
de = ["bzw.", "Abb."]       # applied only when lang resolves to German
```

## `[lint]`

Rule selection for `badness lint`, shared by the [LaTeX](linter-rules.md) and
[BibTeX](bib-linter-rules.md) rule sets. Every rule is on by default. An unknown
rule id is reported at lint time, not rejected at config-parse time.

### `select`

Explicit allowlist of rule ids. When set, only these rules run.

**Default value**: unset (all rules run)

**Type**: array of strings

**Example**:

```toml
[lint]
select = ["deprecated-command", "dollar-display-math"]
```

### `ignore`

Rule ids to disable, applied on top of either [`select`](#select) or the default
rule set.

**Default value**: `[]`

**Type**: array of strings

**Example**:

```toml
[lint]
ignore = ["missing-nonbreaking-space"]
```

## `[build]`

Where the TeX compiler leaves its artifacts. Read by the **language server**
only, which pulls resolved label and section numbers from the `.aux` files for
hover and document symbols; never by the formatter or linter.

### `aux-dir`

Directory holding the build's `.aux` files (latexmk's `-auxdir`/`-outdir`),
resolved relative to the root document's directory when not absolute. When
unset, each document's `.aux` is expected next to it, as in plain
`latex`/`pdflatex` runs.

**Default value**: unset (sibling `.aux` files)

**Type**: path

**Example**:

```toml
[build]
aux-dir = "out"
```

> **Note**: TEXMF-tree discovery (the former `[texmf]` section) is configured
> through your editor's LSP settings, not `badness.toml`. Where a TeX
> installation lives is a fact about the machine, not the project, so it does
> not belong in a file shared across contributors. See [Editor
> Setup](../guide/editor-setup.md#texmf-discovery).