diff --git a/apps/vscode/CHANGELOG.md b/apps/vscode/CHANGELOG.md index 1c76bf70..5a71b400 100644 --- a/apps/vscode/CHANGELOG.md +++ b/apps/vscode/CHANGELOG.md @@ -43,6 +43,8 @@ - Fix issue where format on save could overwrite the contents of a document with incorrect results (). +- Protect code cell options from formatting (). + ## 1.119.0 (Release on 2025-03-21) - Use `QUARTO_VISUAL_EDITOR_CONFIRMED` > `PW_TEST` > `CI` to bypass (`true`) or force (`false`) the Visual Editor confirmation dialogue (). @@ -52,6 +54,8 @@ - Update cell background configuration to add the ability to use the appropriate theme color. The `quarto.cells.background` settings have changed names so you may need to update your configuration (). - Use new command to switch between source and visual editors in Positron (). +- Protect code cell options from formatting (). + ## 1.118.0 (Release on 2024-11-26) - Provide F1 help at cursor in Positron () diff --git a/apps/vscode/src/providers/format.ts b/apps/vscode/src/providers/format.ts index 92c078e5..3a711629 100644 --- a/apps/vscode/src/providers/format.ts +++ b/apps/vscode/src/providers/format.ts @@ -46,6 +46,7 @@ import { virtualDocForLanguage, withVirtualDocUri, } from "../vdoc/vdoc"; +import { languageOptionComment } from "./option"; export function activateCodeFormatting(engine: MarkdownEngine) { @@ -204,7 +205,18 @@ async function formatActiveCell(editor: TextEditor, engine: MarkdownEngine) { } async function formatBlock(doc: TextDocument, block: TokenMath | TokenCodeBlock, language: EmbeddedLanguage) { + const optionComment = languageOptionComment(language.ids[0]) + "| "; const blockLines = lines(codeForExecutableLanguageBlock(block)); + let optionLines = 0; + if (optionComment) { + for (const line of blockLines) { + if (line.startsWith(optionComment)) { + optionLines++; + } else { + break; + } + } + } blockLines.push(""); const vdoc = virtualDocForCode(blockLines, language); const edits = await executeFormatDocumentProvider( @@ -225,7 +237,7 @@ async function formatBlock(doc: TextDocument, block: TokenMath | TokenCodeBlock, ); return new TextEdit(range, edit.newText); }) - .filter(edit => blockRange.contains(edit.range)); + .filter(edit => blockRange.contains(edit.range) && edit.range.start.line > block.range.start.line + optionLines); return adjustedEdits; } } diff --git a/apps/vscode/src/providers/option.ts b/apps/vscode/src/providers/option.ts index eff92603..885322ec 100644 --- a/apps/vscode/src/providers/option.ts +++ b/apps/vscode/src/providers/option.ts @@ -89,7 +89,7 @@ function handleOptionEnter(editor: TextEditor, comment: string) { } } -function languageOptionComment(language: string) { +export function languageOptionComment(language: string) { // some mappings if (language === "ojs") { language = "js"; diff --git a/apps/vscode/src/test/examples/format-python.qmd b/apps/vscode/src/test/examples/format-python.qmd new file mode 100644 index 00000000..5c929cb0 --- /dev/null +++ b/apps/vscode/src/test/examples/format-python.qmd @@ -0,0 +1,10 @@ +--- +title: Formatting Python Code Cells +subtitle: https://github.com/quarto-dev/quarto/pull/655 +format: html +--- + +```{python} +#| label: my-code +x=1;y=2;z=x+y +``` diff --git a/apps/vscode/src/test/formatting.test.ts b/apps/vscode/src/test/formatting.test.ts new file mode 100644 index 00000000..68083fc0 --- /dev/null +++ b/apps/vscode/src/test/formatting.test.ts @@ -0,0 +1,92 @@ +import * as vscode from "vscode"; +import * as assert from "assert"; +import * as path from "path"; +import { openAndShowTextDocument, wait } from "./test-utils"; + +/** + * Creates a document formatting provider from a formatting function. + * @param format - Function that transforms source text + * @returns Document formatting edit provider + */ +function createFormatterFromStringFunc( + format: (sourceText: string) => string +): vscode.DocumentFormattingEditProvider { + return { + provideDocumentFormattingEdits( + document: vscode.TextDocument + ): vscode.TextEdit[] { + const text = document.getText(); + const formatted = format(text); + return [ + new vscode.TextEdit( + new vscode.Range( + document.positionAt(0), + document.positionAt(text.length) + ), + formatted + ), + ]; + }, + }; +} + +/** + * Sets the cursor position in the active editor. + * @param line - Line number + * @param character - Character position + */ +function setCursorPosition(line: number, character: number): void { + const editor = vscode.window.activeTextEditor; + if (editor) { + const position = new vscode.Position(line, character); + editor.selection = new vscode.Selection(position, position); + } +} + +/** + * Tests formatter on a file at a given cursor position. + * @param filename - Name of test file + * @param position - Tuple of line and character position + * @param format - Formatting function + * @returns Formatted document text + */ +async function testFormatter( + filename: string, + [line, character]: [number, number], + format: (sourceText: string) => string +) { + const { doc } = await openAndShowTextDocument(filename); + + const formattingEditProvider = + vscode.languages.registerDocumentFormattingEditProvider( + { scheme: "file", language: "python" }, + createFormatterFromStringFunc(format) + ); + + setCursorPosition(line, character); + await wait(450); + await vscode.commands.executeCommand("quarto.formatCell"); + await wait(450); + + const result = doc.getText(); + formattingEditProvider.dispose(); + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + + return result; +} + +suite("Code Block Formatting", function () { + test("Format Python code block protects options from formatting", async function () { + const formattedResult = await testFormatter( + "format-python.qmd", + [7, 0], + (sourceText: string): string => sourceText.trim() + "\n" + ); + + assert.ok(formattedResult.includes("x = 1"), "Code should be formatted"); + assert.ok( + formattedResult.includes("#| label: my-code"), + "Code Cell option should be preserved" + ); + }); +});