panache 2.13.0

An LSP, formatter, and linter for Pandoc markdown, Quarto, and RMarkdown
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
# Panache <img src='https://raw.githubusercontent.com/jolars/panache/refs/heads/main/images/logo.png' align="right" width="139" />

[![Build and
Test](https://github.com/jolars/panache/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/jolars/panache/actions/workflows/build-and-test.yml)
[![Crates.io](https://img.shields.io/crates/v/panache.svg)](https://crates.io/crates/panache)
[![codecov](https://codecov.io/gh/jolars/panache/graph/badge.svg?token=uaBVOBfILv)](https://codecov.io/gh/jolars/panache)

A formatter, linter, and LSP for Quarto (`.qmd`), Pandoc, and Markdown files.

## Work in Progress

This project is in early development. Expect bugs, missing features, and
breaking changes.

## Installation

### From crates.io (Recommended)

```bash
cargo install panache
```

### Pre-built Binaries

Download pre-built binaries from the [releases
page](https://github.com/jolars/panache/releases). Available for:

- Linux (x86_64, ARM64)
- macOS (Intel, Apple Silicon)
- Windows (x86_64)

Each archive includes the binary, man pages, and shell completions.

### Linux Packages

For Debian/Ubuntu systems:

```bash
# Download the .deb from releases
sudo dpkg -i panache_*.deb
```

For Fedora/RHEL/openSUSE systems:

```bash
# Download the .rpm from releases
sudo rpm -i panache-*.rpm
```

Packages include:

- Binary at `/usr/bin/panache`
- Man pages for all subcommands
- Shell completions (bash, fish, zsh)

## Usage

### Formatting

```bash
# Format a file in place
panache format document.qmd

# Check if a file is formatted
panache format --check document.qmd

# Format from stdin
cat document.qmd | panache format

# Format all .qmd and .md files in directory, recursively
panache format **/*.{qmd,md}
```

### Linting

```bash
# Lint a file
panache lint document.qmd

# Lint entire working directory
panache lint .
```

### Pre-commit Hooks

panache integrates with [pre-commit](https://pre-commit.com/) to automatically
format and lint your files before committing.

**Installation:**

First, install pre-commit if you haven't already:

```bash
pip install pre-commit
# or
brew install pre-commit
```

Then add Panache to your `.pre-commit-config.yaml`:

```yaml
repos:
  - repo: https://github.com/jolars/panache
    rev: v2.6.3 # Use the latest version
    hooks:
      - id: panache-format # Format files
      - id: panache-lint # Lint and auto-fix issues
```

Install the hooks:

```bash
pre-commit install
```

Now Panache will automatically run on your staged `.qmd`, `.md`, and `.Rmd`
files before each commit.

See [examples/pre-commit-config.yaml](examples/pre-commit-config.yaml) for more
configuration options.

## Language Server

Panache includes a built-in LSP implementation for editor integration.

To start the LSP server, run:

```bash
panache lsp
```

But typically you will configure your editor to start the LSP server
automatically when editing supported file types (`.qmd`, `.md`, `.Rmd`).

**Editor Configuration:**

The LSP communicates over stdin/stdout and provides document formatting
capabilities. For Quarto projects, the LSP also reads `_quarto.yml`,
per-directory `_metadata.yml`, and `metadata-files` includes to supply
project-level metadata to bibliography-aware features. For bookdown,
`_bookdown.yml`, `_output.yml`, and `index.Rmd` frontmatter are also considered.

<details>
<summary>Neovim </summary>

Native LSP configuration (0.11+)

```lua
-- .config/nvim/lsp/panache.lua

return {
  cmd = { "panache", "lsp" },
  filetypes = { "quarto", "markdown", "rmarkdown" },
  root_markers = { ".panache.toml", "panache.toml", ".git" },
  settings = {},
}


-- Enable it
vim.lsp.enable({"panache"})
```

Nvim-lspconfig

```lua
-- Add to your LSP config
local lspconfig = require("lspconfig")
local configs = require("lspconfig.configs")

-- Define panache LSP
if not configs.panache then
	configs.panache = {
		default_config = {
			cmd = { "panache", "lsp" },
			filetypes = { "quarto", "markdown", "rmarkdown" },
			root_dir = lspconfig.util.root_pattern(".panache.toml", "panache.toml", ".git"),
			settings = {},
		},
	}
end

-- Enable it
lspconfig.panache.setup({})
```

Format on save:

```lua
vim.api.nvim_create_autocmd("BufWritePre", {
	pattern = { "*.qmd", "*.md", "*.rmd" },
	callback = function()
		vim.lsp.buf.format({ async = false })
	end,
})
```

</details>

<details>
<summary>VS Code</summary>

Install a generic LSP client extension like [vscode-languageserver-node](https://marketplace.visualstudio.com/items?itemName=Microsoft.vscode-languageserver-node), then configure in `settings.json`:

```json
{
  "languageServerExample.server": {
    "command": "panache",
    "args": ["lsp"],
    "filetypes": ["quarto", "markdown", "rmarkdown"]
  },
  "editor.formatOnSave": true
}
```

Or use the [Custom LSP](https://marketplace.visualstudio.com/items?itemName=josa.custom-lsp) extension.

</details>

<details>
<summary>Helix</summary>

Add to `~/.config/helix/languages.toml`:

```toml
[[language]]
name = "markdown"
language-servers = ["panache-lsp"]
auto-format = true

[language-server.panache-lsp]
command = "panache"
args = ["lsp"]
```

</details>

### Features

The list of LSP features supported by Panache is still evolving, but currently
includes:

- Document formatting (full document and range)
- Live diagnostics with quick fixes
- Code actions for refactoring
  - Convert between loose/compact lists
  - Convert between inline/reference footnotes
- Document symbols/outline
- Folding ranges
- Go to definition for references and footnotes

## Configuration

Panache looks for a configuration in:

1. `.panache.toml` or `panache.toml` in current directory or parent directories
2. `$XDG_CONFIG_HOME/panache/config.toml` (usually
   `~/.config/panache/config.toml`)

### Example

```toml
# Markdown flavor and line width
flavor = "quarto"
line-width = 80
line-ending = "auto"

# Formatting style
[style]
wrap = "reflow"

# External code formatters (opt-in)
[formatters]
python = ["isort", "black"] # Sequential formatting
r = "air"                   # Built-in preset
javascript = "prettier"     # Reusable definitions
typescript = "prettier"
yaml = "yamlfmt"            # Formats both code blocks AND frontmatter

# Customize formatters
[formatters.prettier]
prepend-args = ["--print-width=100"]

# External code linters
[linters]
r = "jarl" # Enable R linting
```

See `.panache.toml.example` for a complete configuration reference.

### External Code Formatters

Panache supports external formatters for code blocks—**opt-in and easy to
enable**:

```toml
[formatters]
r = "air"
python = "ruff"
javascript = "prettier"
typescript = "prettier" # Reuse same formatter
```

**Key features:**

- **Opt-in by design** - No surprises, explicit configuration
- **Built-in presets** - Quick setup with sensible defaults
- **Preset inheritance** - Override only specific fields, inherit the rest
- **Incremental arg modification** - Add args with `append_args`/`prepend_args`
- **Sequential formatting** - Run multiple formatters in order:
  `python = ["isort", "black"]`
- **Reusable definitions** - Define once, use for multiple languages
- **Parallel execution** - Formatters run concurrently across languages
- **Graceful fallback** - Missing tools preserve original code (no errors)
- **Custom config** - Full control with `cmd`, `args`, `stdin` fields

**Custom formatter definitions:**

```toml
[formatters]
python = ["isort", "black"]
javascript = "prettier"

# Partial override - inherits cmd/stdin from built-in "air" preset
[formatters.air]
args = ["format", "--custom-flag", "{}"] # Only override args

# Incremental modification - add args without full override
[formatters.ruff]
append_args = ["--line-length", "100"] # Adds to preset args

# Full custom formatter
[formatters.prettier]
cmd = "prettier"
args = ["--print-width=100"]
stdin = true
```

**Preset inheritance:**

When a `[formatters.NAME]` section matches a built-in preset name (like `air`,
`black`, `ruff`), unspecified fields are inherited from the preset:

```toml
[formatters]
r = "air"

[formatters.air]
args = ["format", "--preset=tidyverse"] # cmd and stdin inherited from built-in
```

**Incremental argument modification:**

Use `append_args` and `prepend_args` to add arguments without completely
overriding the base args (from preset or explicit `args` field):

```toml
[formatters]
r = "air"

[formatters.air]
# Base args from preset: ["format", "{}"]
append_args = ["-i", "2"]
# Final args: ["format", "{}", "-i", "2"]
```

Both modifiers work together and with explicit args:

```toml
[formatters.custom]
cmd = "shfmt"
args = ["-filename", "$FILENAME"]
prepend_args = ["--verbose"]
append_args = ["-i", "2"]
# Final: ["--verbose", "-filename", "$FILENAME", "-i", "2"]
```

**Additional details:**

- Formatters respect their own config files (`.prettierrc`, `pyproject.toml`,
  etc.)
- Support both stdin/stdout and file-based formatters
- 30 second timeout per formatter

### External Code Linters

panache supports external linters for code blocks—**opt-in via configuration**:

```toml
# Enable R linting
[linters]
r = "jarl" # R linter with JSON output
```

**Key features:**

- **Opt-in by design** - Only runs if configured
- **Stateful code analysis** - Concatenates all code blocks of same language to
  handle cross-block dependencies
- **LSP integration** - Diagnostics appear inline in your editor
- **CLI support** - `panache lint` shows external linter issues
- **Line-accurate diagnostics** - Reports exact line/column locations

**How it works:**

1. Collects all code blocks of each configured language
2. Concatenates blocks with blank-line preservation (keeps original line
   numbers)
3. Runs external linter on concatenated code
4. Maps diagnostics back to original document positions

**Supported linters:**

- **jarl** - R linter with structured JSON output

**Note:** Auto-fixes from external linters are currently disabled due to byte
offset mapping complexity. Diagnostics work perfectly.

## Motivation

I wanted a formatter that understands Quarto and Pandoc syntax. I have tried to
use Prettier as well as mdformat, but both fail to handle some of the particular
syntax used in Quarto documents, such as fenced divs and some of the table
syntax.

## Design Goals

- Full LSP implementation for editor integration
- Linting as part of LSP but also available as a standalone CLI command
- Support Quarto, Pandoc, and Markdown syntax
- Fast lossless parsing and formatting (no CST changes if already formatted)
- Be configurable, but have sane defaults (that most people can agree on)
- Format math
- Hook into external formatters for code blocks (e.g. `air` for R, `ruff` for
  Python)