From c080d99dce0aae9b980d9cfabdfc843a6b7600fd Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Fri, 4 Jul 2025 10:55:56 +0200 Subject: [PATCH 1/8] Sort text edits by descending start position --- apps/vscode/src/providers/format.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/vscode/src/providers/format.ts b/apps/vscode/src/providers/format.ts index 92c078e5..263340d3 100644 --- a/apps/vscode/src/providers/format.ts +++ b/apps/vscode/src/providers/format.ts @@ -138,9 +138,13 @@ class FormatCellCommand implements Command { const edits = await formatActiveCell(editor, this.engine_); if (edits) { editor.edit((editBuilder) => { - edits.forEach((edit) => { - editBuilder.replace(edit.range, edit.newText); - }); + // Sort edits by descending start position to avoid range shifting issues + edits + .slice() + .sort((a, b) => b.range.start.compareTo(a.range.start)) + .forEach((edit) => { + editBuilder.replace(edit.range, edit.newText); + }); }); } else { window.showInformationMessage( @@ -204,15 +208,21 @@ async function formatActiveCell(editor: TextEditor, engine: MarkdownEngine) { } async function formatBlock(doc: TextDocument, block: TokenMath | TokenCodeBlock, language: EmbeddedLanguage) { + // Create virtual document containing the block const blockLines = lines(codeForExecutableLanguageBlock(block)); blockLines.push(""); const vdoc = virtualDocForCode(blockLines, language); + const edits = await executeFormatDocumentProvider( vdoc, doc, formattingOptions(doc.uri, vdoc.language) ); + if (edits) { + // Because we format with the block code copied in an empty virtual + // document, we need to adjust the ranges to match the edits to the block + // cell in the original file. const blockRange = new Range( new Position(block.range.start.line, block.range.start.character), new Position(block.range.end.line, block.range.end.character) From a2c0dd0c6b43409c4775038f8ad15982e711b19b Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Fri, 4 Jul 2025 12:24:26 +0200 Subject: [PATCH 2/8] Don't append lines to block code before formatting --- apps/vscode/src/providers/format.ts | 3 +-- packages/quarto-core/src/markdown/language.ts | 7 +++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/vscode/src/providers/format.ts b/apps/vscode/src/providers/format.ts index 263340d3..51e53170 100644 --- a/apps/vscode/src/providers/format.ts +++ b/apps/vscode/src/providers/format.ts @@ -209,8 +209,7 @@ async function formatActiveCell(editor: TextEditor, engine: MarkdownEngine) { async function formatBlock(doc: TextDocument, block: TokenMath | TokenCodeBlock, language: EmbeddedLanguage) { // Create virtual document containing the block - const blockLines = lines(codeForExecutableLanguageBlock(block)); - blockLines.push(""); + const blockLines = lines(codeForExecutableLanguageBlock(block, false)); const vdoc = virtualDocForCode(blockLines, language); const edits = await executeFormatDocumentProvider( diff --git a/packages/quarto-core/src/markdown/language.ts b/packages/quarto-core/src/markdown/language.ts index ea3f88fe..201fc259 100644 --- a/packages/quarto-core/src/markdown/language.ts +++ b/packages/quarto-core/src/markdown/language.ts @@ -38,11 +38,14 @@ export function isExecutableLanguageBlock(token: Token) : token is TokenMath | T } } -export function codeForExecutableLanguageBlock(token: TokenMath | TokenCodeBlock) { +export function codeForExecutableLanguageBlock( + token: TokenMath | TokenCodeBlock, + appendNewline = true, +) { if (isMath(token)) { return token.data.text; } else if (isCodeBlock(token)) { - return token.data + "\n"; + return token.data + (appendNewline ? "\n" : ""); } else { return ""; } From 8a6612491382b1302dfacc91c490f9753517aad2 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Fri, 4 Jul 2025 12:33:37 +0200 Subject: [PATCH 3/8] Don't apply partial edits --- apps/vscode/src/providers/format.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/vscode/src/providers/format.ts b/apps/vscode/src/providers/format.ts index 51e53170..c6a5eb84 100644 --- a/apps/vscode/src/providers/format.ts +++ b/apps/vscode/src/providers/format.ts @@ -233,8 +233,17 @@ async function formatBlock(doc: TextDocument, block: TokenMath | TokenCodeBlock, new Position(edit.range.end.line + block.range.start.line + 1, edit.range.end.character) ); return new TextEdit(range, edit.newText); - }) - .filter(edit => blockRange.contains(edit.range)); + }); + + // Bail if any edit is out of range. We used to filter these edits out but + // this could bork the cell. + if (edits.some(edit => !blockRange.contains(edit.range))) { + window.showInformationMessage( + "Formatting edits were out of range and could not be applied to the code cell." + ); + return []; + } + return adjustedEdits; } } From a62fb4a5a1ab8655146e719cda2c935d1d65f943 Mon Sep 17 00:00:00 2001 From: elliot Date: Fri, 26 Sep 2025 16:04:52 -0400 Subject: [PATCH 4/8] Add cell-format test --- apps/vscode/src/test/examples/cell-format.qmd | 5 ++ apps/vscode/src/test/quartoDoc.test.ts | 64 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 apps/vscode/src/test/examples/cell-format.qmd diff --git a/apps/vscode/src/test/examples/cell-format.qmd b/apps/vscode/src/test/examples/cell-format.qmd new file mode 100644 index 00000000..c3fbd011 --- /dev/null +++ b/apps/vscode/src/test/examples/cell-format.qmd @@ -0,0 +1,5 @@ +See https://github.com/quarto-dev/quarto/issues/745 "Reformating R cell in VSCODE with air does not correctly close chunk." + +```{r} +list(foobar, foobar, foobar, foobar, foobar, foobar, foobar, foobar, foobar, foobar, foobar, foobar) +``` \ No newline at end of file diff --git a/apps/vscode/src/test/quartoDoc.test.ts b/apps/vscode/src/test/quartoDoc.test.ts index 9e0c804d..efbfb452 100644 --- a/apps/vscode/src/test/quartoDoc.test.ts +++ b/apps/vscode/src/test/quartoDoc.test.ts @@ -55,8 +55,72 @@ suite("Quarto basics", function () { assert.equal(vscode.window.activeTextEditor, editor, 'quarto extension interferes with other files opened in VSCode!'); }); + + test("quarto.formatCell deals with formatters that do or don't add trailing newline consistently", async function () { + + 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: 'r' }, + 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; + } + + const newlineFormatterResult = await testFormatter( + "cell-format.qmd", + [3, 1], + (sourceText) => sourceText + 'FORMAT SUCCESS\n' + ); + const noopFormatterResult = await testFormatter( + "cell-format.qmd", + [3, 1], + (sourceText) => sourceText + 'FORMAT SUCCESS' + ); + + assert.ok(newlineFormatterResult.includes('FORMAT SUCCESS'), 'newlineFormatter failed'); + assert.ok(noopFormatterResult.includes('FORMAT SUCCESS'), 'noopFormatter failed'); + + assert.equal(newlineFormatterResult, noopFormatterResult); + }); + + suiteTeardown(() => { + vscode.window.showInformationMessage('All tests done!'); + }); }); +function createFormatterFromStringFunc(format: (sourceText: string) => string) { + return { + provideDocumentFormattingEdits(document: vscode.TextDocument): vscode.ProviderResult { + const fileStart = new vscode.Position(0, 0); + const fileEnd = document.lineAt(document.lineCount - 1).range.end; + + return [new vscode.TextEdit(new vscode.Range(fileStart, fileEnd), format(document.getText()))]; + } + }; +} + +function setCursorPosition(line: number, character: number) { + const editor = vscode.window.activeTextEditor; + if (editor) { + const position = new vscode.Position(line, character); + const newSelection = new vscode.Selection(position, position); + editor.selection = newSelection; + editor.revealRange(newSelection, vscode.TextEditorRevealType.InCenter); // Optional: scroll to the new position + } +} + /** * * When the test is run on the dev's machine for the first time, saves the roundtripped file as a snapshot. From 89a013ed5864e34990eee62515e8b84426772fd6 Mon Sep 17 00:00:00 2001 From: elliot Date: Tue, 20 Jan 2026 15:47:29 -0500 Subject: [PATCH 5/8] don't append newline in vdoc, fix edits out-of-range check --- apps/vscode/src/providers/format.ts | 4 ++-- apps/vscode/src/vdoc/vdoc.ts | 14 +++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/apps/vscode/src/providers/format.ts b/apps/vscode/src/providers/format.ts index c6a5eb84..4d7fc8e0 100644 --- a/apps/vscode/src/providers/format.ts +++ b/apps/vscode/src/providers/format.ts @@ -210,7 +210,7 @@ async function formatActiveCell(editor: TextEditor, engine: MarkdownEngine) { async function formatBlock(doc: TextDocument, block: TokenMath | TokenCodeBlock, language: EmbeddedLanguage) { // Create virtual document containing the block const blockLines = lines(codeForExecutableLanguageBlock(block, false)); - const vdoc = virtualDocForCode(blockLines, language); + const vdoc = virtualDocForCode(blockLines, language, false); const edits = await executeFormatDocumentProvider( vdoc, @@ -237,7 +237,7 @@ async function formatBlock(doc: TextDocument, block: TokenMath | TokenCodeBlock, // Bail if any edit is out of range. We used to filter these edits out but // this could bork the cell. - if (edits.some(edit => !blockRange.contains(edit.range))) { + if (adjustedEdits.some(edit => !blockRange.contains(edit.range))) { window.showInformationMessage( "Formatting edits were out of range and could not be applied to the code cell." ); diff --git a/apps/vscode/src/vdoc/vdoc.ts b/apps/vscode/src/vdoc/vdoc.ts index b1830bc1..1eb5efde 100644 --- a/apps/vscode/src/vdoc/vdoc.ts +++ b/apps/vscode/src/vdoc/vdoc.ts @@ -98,17 +98,13 @@ function padLinesForLanguage(lines: string[], language: EmbeddedLanguage) { } } -export function virtualDocForCode(code: string[], language: EmbeddedLanguage) { - - const lines = [...code]; - - if (language.inject) { - lines.unshift(...language.inject); - } - +export function virtualDocForCode(code: string[], language: EmbeddedLanguage, appendNewline: boolean = true) { return { language, - content: lines.join("\n") + "\n", + content: [ + ...(language?.inject ?? []), + ...code + ].join("\n") + (appendNewline ? '\n' : ''), }; } From 983c59312032cdd085ede9ceff2b78aaa92a855c Mon Sep 17 00:00:00 2001 From: elliot Date: Tue, 20 Jan 2026 16:52:05 -0500 Subject: [PATCH 6/8] Remove test --- apps/vscode/src/test/examples/cell-format.qmd | 5 -- apps/vscode/src/test/quartoDoc.test.ts | 64 ------------------- 2 files changed, 69 deletions(-) delete mode 100644 apps/vscode/src/test/examples/cell-format.qmd diff --git a/apps/vscode/src/test/examples/cell-format.qmd b/apps/vscode/src/test/examples/cell-format.qmd deleted file mode 100644 index c3fbd011..00000000 --- a/apps/vscode/src/test/examples/cell-format.qmd +++ /dev/null @@ -1,5 +0,0 @@ -See https://github.com/quarto-dev/quarto/issues/745 "Reformating R cell in VSCODE with air does not correctly close chunk." - -```{r} -list(foobar, foobar, foobar, foobar, foobar, foobar, foobar, foobar, foobar, foobar, foobar, foobar) -``` \ No newline at end of file diff --git a/apps/vscode/src/test/quartoDoc.test.ts b/apps/vscode/src/test/quartoDoc.test.ts index efbfb452..9e0c804d 100644 --- a/apps/vscode/src/test/quartoDoc.test.ts +++ b/apps/vscode/src/test/quartoDoc.test.ts @@ -55,72 +55,8 @@ suite("Quarto basics", function () { assert.equal(vscode.window.activeTextEditor, editor, 'quarto extension interferes with other files opened in VSCode!'); }); - - test("quarto.formatCell deals with formatters that do or don't add trailing newline consistently", async function () { - - 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: 'r' }, - 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; - } - - const newlineFormatterResult = await testFormatter( - "cell-format.qmd", - [3, 1], - (sourceText) => sourceText + 'FORMAT SUCCESS\n' - ); - const noopFormatterResult = await testFormatter( - "cell-format.qmd", - [3, 1], - (sourceText) => sourceText + 'FORMAT SUCCESS' - ); - - assert.ok(newlineFormatterResult.includes('FORMAT SUCCESS'), 'newlineFormatter failed'); - assert.ok(noopFormatterResult.includes('FORMAT SUCCESS'), 'noopFormatter failed'); - - assert.equal(newlineFormatterResult, noopFormatterResult); - }); - - suiteTeardown(() => { - vscode.window.showInformationMessage('All tests done!'); - }); }); -function createFormatterFromStringFunc(format: (sourceText: string) => string) { - return { - provideDocumentFormattingEdits(document: vscode.TextDocument): vscode.ProviderResult { - const fileStart = new vscode.Position(0, 0); - const fileEnd = document.lineAt(document.lineCount - 1).range.end; - - return [new vscode.TextEdit(new vscode.Range(fileStart, fileEnd), format(document.getText()))]; - } - }; -} - -function setCursorPosition(line: number, character: number) { - const editor = vscode.window.activeTextEditor; - if (editor) { - const position = new vscode.Position(line, character); - const newSelection = new vscode.Selection(position, position); - editor.selection = newSelection; - editor.revealRange(newSelection, vscode.TextEditorRevealType.InCenter); // Optional: scroll to the new position - } -} - /** * * When the test is run on the dev's machine for the first time, saves the roundtripped file as a snapshot. From 9773b2627e016c21649c348f64ce2b5f47e39a4c Mon Sep 17 00:00:00 2001 From: elliot Date: Tue, 20 Jan 2026 16:56:31 -0500 Subject: [PATCH 7/8] Undo unecessary `virtualDocForCode` changes --- apps/vscode/src/providers/format.ts | 2 +- apps/vscode/src/vdoc/vdoc.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/vscode/src/providers/format.ts b/apps/vscode/src/providers/format.ts index 4d7fc8e0..a8a3663d 100644 --- a/apps/vscode/src/providers/format.ts +++ b/apps/vscode/src/providers/format.ts @@ -210,7 +210,7 @@ async function formatActiveCell(editor: TextEditor, engine: MarkdownEngine) { async function formatBlock(doc: TextDocument, block: TokenMath | TokenCodeBlock, language: EmbeddedLanguage) { // Create virtual document containing the block const blockLines = lines(codeForExecutableLanguageBlock(block, false)); - const vdoc = virtualDocForCode(blockLines, language, false); + const vdoc = virtualDocForCode(blockLines, language); const edits = await executeFormatDocumentProvider( vdoc, diff --git a/apps/vscode/src/vdoc/vdoc.ts b/apps/vscode/src/vdoc/vdoc.ts index 1eb5efde..b1830bc1 100644 --- a/apps/vscode/src/vdoc/vdoc.ts +++ b/apps/vscode/src/vdoc/vdoc.ts @@ -98,13 +98,17 @@ function padLinesForLanguage(lines: string[], language: EmbeddedLanguage) { } } -export function virtualDocForCode(code: string[], language: EmbeddedLanguage, appendNewline: boolean = true) { +export function virtualDocForCode(code: string[], language: EmbeddedLanguage) { + + const lines = [...code]; + + if (language.inject) { + lines.unshift(...language.inject); + } + return { language, - content: [ - ...(language?.inject ?? []), - ...code - ].join("\n") + (appendNewline ? '\n' : ''), + content: lines.join("\n") + "\n", }; } From dfcb5de2e52af47e2cb4beb4aa1769e9ad62c0f0 Mon Sep 17 00:00:00 2001 From: elliot Date: Thu, 22 Jan 2026 10:00:13 -0500 Subject: [PATCH 8/8] Add changelog entry --- apps/vscode/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/vscode/CHANGELOG.md b/apps/vscode/CHANGELOG.md index 2f7505a6..f256a236 100644 --- a/apps/vscode/CHANGELOG.md +++ b/apps/vscode/CHANGELOG.md @@ -5,6 +5,7 @@ - Fixed Copilot completions in `.qmd` documents (). - Fixed a bug where the `autoDetectColorScheme` setting could cause equation previews to have a dark text on dark background and vice versa (). - Fix a regression where bash cell execution does not work (). +- Fix cell formatting sometimes deleting code at the end of the cell (). ## 1.128.0 (Release on 2026-01-08)