Skip to content

fix: protect code cell options from formatting#655

Open
mcanouil wants to merge 8 commits intoquarto-dev:mainfrom
mcanouil:feat/protect-options-formatting
Open

fix: protect code cell options from formatting#655
mcanouil wants to merge 8 commits intoquarto-dev:mainfrom
mcanouil:feat/protect-options-formatting

Conversation

@mcanouil
Copy link
Copy Markdown
Contributor

@mcanouil mcanouil commented Feb 8, 2025

Implement a mechanism to prevent code cell options from being formatted, ensuring that only relevant code is affected during formatting operations.

Fixes #368, fixes #135

@mcanouil mcanouil marked this pull request as ready for review February 9, 2025 15:13
@cscheid
Copy link
Copy Markdown
Contributor

cscheid commented Feb 21, 2025

Thanks for the PR.

I assume this was done to prevent black and other Python auto-formatting tools from mangling our #| syntax. Is that the case? If so, I wonder if a more targeted, Python-only fix is better.

@cscheid
Copy link
Copy Markdown
Contributor

cscheid commented Feb 21, 2025

Another thought here is that this kind of PR really makes me wish we had a test suite in this repo.

@mcanouil
Copy link
Copy Markdown
Contributor Author

mcanouil commented Feb 21, 2025

I assume this was done to prevent black and other Python auto-formatting tools from mangling our #| syntax. Is that the case? If so, I wonder if a more targeted, Python-only fix is better.

I was thinking about making it specific at first but then I realised that the expected comments are all hardcoded, so I reconsidered, and made a general protection for the comments symbols (hardcoded in two places).

Another thought here is that this kind of PR really makes me wish we had a test suite in this repo.

I'm not yet very familiar on this part, but I can't agree more with this.
That's on my personal roadmap before releasing Quarto Wizard 1.0.0.

I did "manual tests" as far as this approach can go.

@mcanouil
Copy link
Copy Markdown
Contributor Author

Let's hold on on this PR as the behaviour is not new at all so it can wait a bit for me to implement a test soon as I get some spare time.

@vezwork
Copy link
Copy Markdown
Collaborator

vezwork commented Sep 9, 2025

@mcanouil we now have extension tests! If you're interested in merging this PR, want to add some tests?

@juliasilge
Copy link
Copy Markdown
Collaborator

We are continuing to get more user reports of this such as in posit-dev/positron#9432, so I'd love to see a test written here so we can merge this fix in!

@mcanouil
Copy link
Copy Markdown
Contributor Author

mcanouil commented Sep 18, 2025

I'll try to make some time to make this happen.

@mcanouil
Copy link
Copy Markdown
Contributor Author

mcanouil commented Oct 5, 2025

I've setup the test, but I can't get to make the formatting command to work.
I'm missing something, maybe related to vdoc or something else ...

npx @vscode/test-cli --grep "Code Block Formatting" --install-extensions "ms-python.black-formatter"

I don't know how to trigger all the logic of the formatCellCommand

The easiest would be to export formatActiveCell(), but we don't want to export that only for a test.

I've also tried to trigger the whole document formatting, but it does not work for an unknown reason.

I have to drop this for now, but if someone has a better understand of the whole formatting logic of the extension (not that trivial) and knows how to trigger the activation, feel free to take over.

@juliasilge
Copy link
Copy Markdown
Collaborator

juliasilge commented Oct 5, 2025

Ah, I don't think we can use any other extensions in the tests, because of the way that they use a special build of VS Code that is specially built for running extension tests.

Instead, what we'll want to do is mock out a formatter to run in the tests. This is the same thing that #754 needs. We may want to put the formatting mock somewhere that all tests can use it.

I also think that exporting a function to use in a test is a very good/reasonable way to go.

@mcanouil
Copy link
Copy Markdown
Contributor Author

mcanouil commented Oct 5, 2025

Well, @vscode/test-cli does support installing extensions and it is used here, even when calling via yarn.

I don't think the issue is the presence/absence of the formatter.

I can only make the test install the extension to be sure yarn run test has it when running.

@mcanouil
Copy link
Copy Markdown
Contributor Author

mcanouil commented Oct 5, 2025

Ok, now I have some information. The vscode-test instance shows a notification that the selection marker is not within a cell that supports formatting which is clearly wrong, but no clue why quarto displays this.

"Editor selection is not within a code cell that supports formatting."

from FormatCellCommand.
So now the command is triggered.

yarn run test --grep "Code Block Formatting" 

@vezwork
Copy link
Copy Markdown
Collaborator

vezwork commented Oct 15, 2025

For reference: In another PR I added a mocked cell formatter in a test here: 873fcab

@mcanouil
Copy link
Copy Markdown
Contributor Author

@vezwork Thanks, I'll try to take a look next week-end and see to finalise this PR.

@mcanouil
Copy link
Copy Markdown
Contributor Author

mcanouil commented Oct 19, 2025

Even changing to your testFormatter function does not work.
Note that the formatting do work as it should but I really don't understand what is going on with this VSCode extension during testing.

See when running extension debug mode:

Screen.Recording.2025-10-19.at.19.10.58.mov

I have to drop this as It costs me way too much time for no good reasons.

Feel free to take over as I won't spend more time on this for the next few months at least.

@vezwork
Copy link
Copy Markdown
Collaborator

vezwork commented Nov 13, 2025

I've looked into this PR a bit and I think it has some issues that intermingle with some of the issues that #754 is trying to fix. I will try to get #754 working before coming back to this one.

…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.
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.
@mcanouil mcanouil force-pushed the feat/protect-options-formatting branch from 446a935 to dfcc6a7 Compare April 11, 2026 19:48
…ock 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.
…matting

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.
…t 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.
- 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.
- 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.
@mcanouil mcanouil marked this pull request as ready for review April 12, 2026 10:30
@mcanouil
Copy link
Copy Markdown
Contributor Author

I took another stab at this with some help this time (roborev + claude code for review).
It now works for select, code cell, and document-level formatting.

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.

formatting document in VSCode with Quarto breaks option syntax [vscode] Python formatter breaks comment pipe #| on code blocks

4 participants