You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: protect code cell options from formatting (#655)
* fix(format): strip cell option directives before formatting embedded code
Quarto code cells carry metadata on leading comment lines (`#| label:
foo`, `#| echo: false`, `//| label: bar`, ...) that must survive
verbatim so Quarto can parse them on the next render. Previously,
`formatBlock` handed the entire cell to the language formatter (Black,
autopep8, styler, ...), which treats these lines as ordinary comments
and may reflow, reorder, or rewrite them.
Hide the option lines from the formatter entirely by slicing them off
`blockLines` before calling `virtualDocForCode`, and shift the returned
edits back into the real code range via `block.range.start.line + 1 +
optionLines`. Blocks that are entirely option directives short-circuit
to `undefined` (no formatter invocation, no out-of-range toast).
This replaces the filter-after approach that dropped any edit whose
start line landed in the option region, which quietly swallowed valid
multi-line edits and block-wide edits starting at virtual-doc line 0.
* test(format): cover cell option protection with deterministic fixtures
Add a `Code Block Formatting` suite that registers a fake Python
formatter from a pure `string -> string` function, so each case can
prove exactly what did and did not get rewritten. The fake formatters
mangle `=` and `+` into spaced forms, rewrite `#| ` to `# PIPE`, and
normalise comment spacing.
Cover six scenarios: a single directive followed by code, multiple
directives preserved in original order, a block with no directives,
a directives-only block (no-op), multiline code with badly-formatted
standalone and inline comments, and a hostile formatter run against a
mixed block to prove the directives never reach the formatter at all.
* docs: note cell option formatting fix in CHANGELOG
* fix(format): protect option directives via canonical regex and per-block formatting
Close three gaps discovered in adversarial review of the initial fix:
1. The `languageOptionComment` helper used a private map keyed by short
language ids (e.g. `python`, `r`, `js`). It didn't know about
`typescript`, so `//| ...` directives in `ts` cells were never
stripped and still reached the formatter. Use `language.comment` from
editor-core instead, which is the canonical, language-wide mapping.
2. The option-line detection used `startsWith("<comment>| ")`, which
only matched the canonical form. Quarto's own cell-option parser
(`cell/options.ts`) accepts `^<comment>\s*\| ?`, so variants like
`#|label`, `# | label`, and `#| label` are legal. Switch to the
same canonical regex so every variant is protected.
3. `embeddedDocumentFormattingProvider` bypassed `formatBlock` entirely
for languages with `canFormatDocument !== false` (R, Julia, TS, ...),
handing the whole virtualised document to the formatter. Option
lines inside every block leaked through. Route every target block
through `formatBlock` instead, so the same strip-before-format path
protects both cell-format and document-format invocations.
* test(format): cover whitespace variants, typescript, and document formatting
Add three fixtures and three regression tests for the gaps fixed in the
previous commit:
- `format-python-option-variants.qmd` exercises every whitespace
variant the Quarto cell-option parser accepts (`#|label`,
`# | label`, `#| label`). The accompanying test runs an aggressive
formatter that rewrites any `#\s*\|` line to `# MANGLED`; with the
canonical-regex strip in place, no directive ever reaches the
formatter, so `# MANGLED` must not appear.
- `format-typescript.qmd` covers the `//|` directive form. Before
switching to `language.comment` the `ts` language had no entry in
the private comment map and the directive was unprotected; this
test would have silently allowed the hostile rewrite.
- `format-r-multiple-blocks.qmd` drives the document-formatting path
(formerly bypassed `formatBlock` for languages with `canFormatDocument
!== false`). The test invokes `embeddedDocumentFormattingProvider`
directly since the LSP middleware isn't wired up in the vscode-test
host, and asserts that every block's directives survive while the
code itself is reformatted.
The two new helpers `hostileRFormatter` and `hostileTypeScriptFormatter`
use the same canonical regex pattern to decide what to mangle, so they
are realistic stand-ins for any formatter that treats `#|`/`//|` lines
as ordinary comments.
* refactor(format): reuse canonical option pattern and harden doc-format path
Address adversarial review findings on the option-protection patch:
- Reuse `optionCommentPattern` from `cell/options.ts` instead of
re-encoding the regex in `format.ts`. The protection path can no
longer drift from Quarto's own cell-option parser, and the local
`escapeRegExp` helper is gone.
- Make document formatting all-or-nothing: aggregate per-block bails
into a single message and abort the whole operation rather than
apply a partial format. Per-block toasts are silenced via a new
`silentOutOfRange` flag on `formatBlock` so the doc path no longer
spams a message per failing block.
- Thread `defaultOptions` through `embeddedDocumentRangeFormattingProvider`
and `FormatCellCommand`, both of which previously fell back to a
hardcoded `tabSize: 4 / insertSpaces: true` regardless of the user's
editor settings.
- Mark `languageOptionComment` as private in `option.ts` since it has
no remaining external consumers.
* test(format): exercise real edit-apply path and multi-edit aggregation
- Replace the direct provider call in the document-formatting test
with a real `vscode.executeFormatDocumentProvider` invocation. The
test now registers `embeddedDocumentFormattingProvider` against the
`quarto` language and lets VS Code's command machinery validate
ordering and overlap. Pairwise non-overlap is asserted explicitly so
a future regression in offset arithmetic surfaces here.
- Add a test that exercises a formatter returning one `TextEdit` per
non-empty line, covering the multi-edit aggregation path that all
previous hostile formatters skipped.
- Wrap both new tests in try/finally so a failed assertion can never
leak a registered formatter into a later suite.
- Drop the misleading comment about inject lines from
`mangleHashPipeLines` and remove an unnecessary `wait(450)` from
the document-formatting test.
* fix(format): harden edge cases from adversarial review
- Treat empty formatter results as no-op (not out-of-range bail) in both
formatBlock and FormatCellCommand, preventing spurious error toasts.
- Use trim() in the options-only guard so whitespace-only tails after
option directives are correctly treated as empty.
- Wrap testFormatter disposal in try/finally to prevent leaked formatter
registrations on test failure.
- Document why block-comment languages are not affected by the option
stripping logic.
---------
Co-authored-by: Elliot <key.draw@gmail.com>
Copy file name to clipboardExpand all lines: apps/vscode/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,6 +5,7 @@
5
5
- Added support for Positron's statement execution feature that reports the approximate line number of the parse error (<https://github.com/quarto-dev/quarto/pull/919>).
6
6
- Fixed a bug where `Quarto: Format Cell` would notify you that no formatter was available for code cells that were already formatted (<https://github.com/quarto-dev/quarto/pull/933>).
7
7
- No longer claim `.typ` files. Typst syntax highlighting in Quarto documents is unaffected, but standalone Typst files are now left to dedicated extensions like Tinymist (<https://github.com/quarto-dev/quarto/pull/943>).
8
+
- Preserve Quarto code cell option directives (e.g. `#| label: foo`) when formatting embedded code. The directives are now stripped from the virtual document before being handed to the language formatter, so formatters such as Black, autopep8, and styler can no longer reflow or rewrite them (<https://github.com/quarto-dev/quarto/pull/655>).
8
9
- Fixed a bug where closing the Quarto Preview terminal via the trash icon did not clean up intermediate `.quarto_ipynb` files (<https://github.com/quarto-dev/quarto/pull/947>).
0 commit comments