From ebb108cfae8b3a09ba9aceb908c20b853864bd67 Mon Sep 17 00:00:00 2001 From: seem Date: Mon, 15 Jun 2026 08:08:22 +0200 Subject: [PATCH 1/5] fix preview in notebooks This fixes a bug where notebooks were failing to preview with error 'No Quarto document available to render'. The problem is that notebook cell text editors disappear from `vscode.window.visibleTextEditors` when either the bottom Panel or the Secondary Sidebar are visible. I'm unsure what the cause is, but it seems like a Code OSS bug or perhaps intentional change in behavior. The fix is to stop requiring a text editor in the notebook case. `QuartoEditor.textEditor` is only used in one place today: `PreviewManager.detectErrorNavigation` which is already skipped for notebooks, so it's safe to remove for notebooks. This allows us to remove the `visibleTextEditors` check. Since `QuartoEditor` still requires a `TextDocument` reference, we now use the first notebook cell, if one exists. Fixes #1006. --- apps/vscode/src/core/doc.ts | 45 ++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/apps/vscode/src/core/doc.ts b/apps/vscode/src/core/doc.ts index 1ad3cf72..14bf3100 100644 --- a/apps/vscode/src/core/doc.ts +++ b/apps/vscode/src/core/doc.ts @@ -164,7 +164,7 @@ export function preserveEditorFocus(editor?: QuartoEditor) { editor = editor || (vscode.window.activeTextEditor - ? quartoEditor(vscode.window.activeTextEditor) + ? quartoTextEditor(vscode.window.activeTextEditor) : undefined); if (editor) { if (!isNotebook(editor?.document)) { @@ -217,11 +217,9 @@ export function findQuartoEditor( | vscode.NotebookDocument | undefined; if (notebookDocument) { - const textEditor = vscode.window.visibleTextEditors.find((editor) => { - return editor.document.uri.fsPath.includes(notebookDocument.uri.fsPath); - }); - if (textEditor && filter(textEditor.document)) { - return quartoEditor(textEditor, engine, context, notebookDocument); + const firstCellDocument = notebookDocument.cellAt(0)?.document; + if (firstCellDocument && filter(firstCellDocument)) { + return quartoNotebookEditor(notebookEditor, firstCellDocument); } } } @@ -229,7 +227,7 @@ export function findQuartoEditor( // active text editor const textEditor = vscode.window.activeTextEditor; if (textEditor && filter(textEditor.document)) { - return quartoEditor(textEditor, engine, context); + return quartoTextEditor(textEditor, engine, context); // check visible text editors } else if (includeVisible) { // visible visual editor (sometime it loses track of 'active' so we need to use 'visible') @@ -243,7 +241,7 @@ export function findQuartoEditor( filter(editor.document) ); if (visibleEditor) { - return quartoEditor(visibleEditor, engine, context); + return quartoTextEditor(visibleEditor, engine, context); } else { return undefined; } @@ -252,12 +250,12 @@ export function findQuartoEditor( } } -export function quartoEditor( +function quartoTextEditor( editor: vscode.TextEditor, engine?: MarkdownEngine, context?: QuartoContext, notebook?: NotebookDocument -) { +): QuartoEditor { return { document: editor.document, activate: async () => { @@ -285,6 +283,33 @@ export function quartoEditor( }; } +function quartoNotebookEditor( + editor: vscode.NotebookEditor, + firstCellDocument: vscode.TextDocument, +): QuartoEditor { + return { + document: firstCellDocument, + activate: async () => { + // TODO: This should probably use showNotebookDocument. + // And we could probably also activate() notebook editors + // in many places where we currently skip notebooks. + // We're leaving it as showTextDocument for now to focus + // the current PR on fixing preview for notebooks. + await vscode.window.showTextDocument( + firstCellDocument, + editor.viewColumn, + false + ); + }, + slideIndex: async () => { + // Throwing is safe, since the only place slideIndex is called today + // skips notebooks. + throw new Error("slideIndex not supported for notebook editors"); + }, + notebook: editor.notebook, + }; +} + async function tryResolveUriToQuartoDoc( resource: vscode.Uri ): Promise { From 42044cec08f7440960cf7b0c817b6b2b14757507 Mon Sep 17 00:00:00 2001 From: seem Date: Mon, 15 Jun 2026 08:43:34 +0200 Subject: [PATCH 2/5] fix: run preview from visible but not active notebook editor --- apps/vscode/src/core/doc.ts | 48 +++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/apps/vscode/src/core/doc.ts b/apps/vscode/src/core/doc.ts index 14bf3100..81e04295 100644 --- a/apps/vscode/src/core/doc.ts +++ b/apps/vscode/src/core/doc.ts @@ -228,26 +228,34 @@ export function findQuartoEditor( const textEditor = vscode.window.activeTextEditor; if (textEditor && filter(textEditor.document)) { return quartoTextEditor(textEditor, engine, context); - // check visible text editors - } else if (includeVisible) { - // visible visual editor (sometime it loses track of 'active' so we need to use 'visible') - const visibleVisualEditor = VisualEditorProvider.activeEditor(true); - if (visibleVisualEditor && filter(visibleVisualEditor.document)) { - return visibleVisualEditor; - } + } - // visible text editors - const visibleEditor = vscode.window.visibleTextEditors.find((editor) => - filter(editor.document) - ); - if (visibleEditor) { - return quartoTextEditor(visibleEditor, engine, context); - } else { - return undefined; - } - } else { - return undefined; + // check visible editors + + // visible visual editor (sometime it loses track of 'active' so we need to use 'visible') + const visibleVisualEditor = VisualEditorProvider.activeEditor(true); + if (visibleVisualEditor && filter(visibleVisualEditor.document)) { + return visibleVisualEditor; } + + // visible notebook editors + const visibleNotebookEditor = vscode.window.visibleNotebookEditors.find((editor) => + filter(editor.notebook.cellAt(0)?.document) + ); + if (visibleNotebookEditor) { + const firstCellDocument = visibleNotebookEditor.notebook.cellAt(0).document; + return quartoNotebookEditor(visibleNotebookEditor, firstCellDocument); + } + + // visible text editors + const visibleEditor = vscode.window.visibleTextEditors.find((editor) => + filter(editor.document) + ); + if (visibleEditor) { + return quartoTextEditor(visibleEditor, engine, context); + } + + return undefined; } function quartoTextEditor( @@ -293,8 +301,8 @@ function quartoNotebookEditor( // TODO: This should probably use showNotebookDocument. // And we could probably also activate() notebook editors // in many places where we currently skip notebooks. - // We're leaving it as showTextDocument for now to focus - // the current PR on fixing preview for notebooks. + // We're leaving it as showTextDocument for now to minimize + // the number of changes in this PR focused on #1006. await vscode.window.showTextDocument( firstCellDocument, editor.viewColumn, From a969663c251d03453fc5818fcfe22d9435fca792 Mon Sep 17 00:00:00 2001 From: seem Date: Wed, 17 Jun 2026 14:42:30 +0200 Subject: [PATCH 3/5] test and changelog --- apps/vscode/CHANGELOG.md | 1 + apps/vscode/src/test/examples/hello.ipynb | 29 +++ apps/vscode/src/test/quartoEditor.test.ts | 230 ++++++++++++++++++++++ apps/vscode/src/test/test-utils.ts | 23 ++- 4 files changed, 276 insertions(+), 7 deletions(-) create mode 100644 apps/vscode/src/test/examples/hello.ipynb create mode 100644 apps/vscode/src/test/quartoEditor.test.ts diff --git a/apps/vscode/CHANGELOG.md b/apps/vscode/CHANGELOG.md index a17e0d8f..95fcce61 100644 --- a/apps/vscode/CHANGELOG.md +++ b/apps/vscode/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.134.0 (Unreleased) - The "Preview" and "Preview Format..." commands now show in the Positron Notebook Editor overflow menu (). +- Fixed notebooks failing to render or preview when they were visible but not active, or while the Panel or Sidebar were open (). - In Positron, Jupyter Notebooks (`.ipynb`) are now exported via the new unified "Export" command, rather than the "Quarto: Convert to .qmd" command (). - Fixed a bug where formatting a code cell stripped leading empty lines. Leading empty lines between option directives and code are now preserved, and two or more leading empty lines are collapsed to one (). - Fixed a bug where IPython magics (`%`, `%%`) and shell escapes (`!`) in Python code cells produced spurious diagnostics from language servers like Pyrefly and Ruff. These lines are now commented out in the virtual document handed to language servers (). diff --git a/apps/vscode/src/test/examples/hello.ipynb b/apps/vscode/src/test/examples/hello.ipynb new file mode 100644 index 00000000..2514c48e --- /dev/null +++ b/apps/vscode/src/test/examples/hello.ipynb @@ -0,0 +1,29 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e4e2834b", + "metadata": {}, + "source": [ + "# Heading" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1181b7a", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Hello, world!\")" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/apps/vscode/src/test/quartoEditor.test.ts b/apps/vscode/src/test/quartoEditor.test.ts new file mode 100644 index 00000000..7898912d --- /dev/null +++ b/apps/vscode/src/test/quartoEditor.test.ts @@ -0,0 +1,230 @@ +/* + * quartoEditor.test.ts + * + * Copyright (C) 2026 by Posit Software, PBC + * + * Unless you have received this program directly from Posit Software pursuant + * to the terms of a commercial license agreement with Posit Software, then + * this program is licensed to you under the terms of version 3 of the + * GNU Affero General Public License. This program is distributed WITHOUT + * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the + * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. + * + */ + +import * as assert from "assert"; +import * as vscode from "vscode"; +import { APPROX_TIME_TO_OPEN_VISUAL_EDITOR, examplesUri, openAndShowExamplesTextDocument, wait } from "./test-utils"; +import { canPreviewDoc, findQuartoEditor, QuartoEditor } from "../core/doc"; +import { MarkdownEngine } from "../markdown/engine"; +import { initQuartoContext } from "quarto-core"; +import { VisualEditorProvider } from "../providers/editor/editor"; + +suite('Quarto Editor', function () { + suite('findQuartoEditor', function () { + teardown(async function () { + // Close all editors after each test to ensure a clean slate for the next test. + await vscode.commands.executeCommand("workbench.action.closeAllEditors"); + }); + + test('returns undefined when there is no active or visible .qmd text editor', async function () { + await openAndShowExamplesTextDocument("hello.lua"); + + const quartoEditor = findQuartoEditorWithDefaults(); + + assert.strictEqual( + quartoEditor, + undefined, + "Expected not to find a Quarto editor when there are no .qmd text editors" + ); + }); + + test('finds the active .qmd text editor', async function () { + const { editor: textEditor } = await openAndShowExamplesTextDocument("hello.qmd"); + await openAndShowExamplesTextDocument("simple-divs.qmd", { viewColumn: vscode.ViewColumn.Beside, preserveFocus: true }); + + const quartoEditor = findQuartoEditorWithDefaults(); + + + assertQuartoEditorForTextEditor(quartoEditor, textEditor); + }); + + test('finds a visible (inactive) .qmd text editor', async function () { + const { editor: textEditor } = await openAndShowExamplesTextDocument("hello.qmd"); + await openAndShowExamplesTextDocument("hello.lua", { viewColumn: vscode.ViewColumn.Beside, preserveFocus: false }); + + const quartoEditor = findQuartoEditorWithDefaults(); + + assertQuartoEditorForTextEditor(quartoEditor, textEditor); + }); + + // TODO: This test is really hard to write because we currently run extension tests + // against the bundled extension, so our `findQuartoEditor` uses a totally separate + // copy of `VisualEditorProvider` than the extension, so we always get `undefined` + // from `VisualEditorProvider.activeEditor()`. + test.skip('finds the active .qmd visual editor', async function () { + const uri = examplesUri("hello.qmd"); + await vscode.commands.executeCommand("quarto.test_setkVisualModeConfirmedTrue"); + await vscode.commands.executeCommand( + "vscode.openWith", + uri, + VisualEditorProvider.viewType + ); + + await vscode.commands.executeCommand( + "vscode.openWith", + examplesUri("simple-divs.qmd"), + VisualEditorProvider.viewType, + { viewColumn: vscode.ViewColumn.Beside, preserveFocus: true } + ); + + // TODO: Could we hook into an event instead? Currently really hard because of the + // issue noted above. + await wait(APPROX_TIME_TO_OPEN_VISUAL_EDITOR); + + const quartoEditor = findQuartoEditorWithDefaults(); + + assertQuartoEditorForVisualEditor(quartoEditor, uri, vscode.ViewColumn.One); + }); + + test.skip('finds a visible (inactive) .qmd visual editor', async function () { + const uri = examplesUri("hello.qmd"); + await vscode.commands.executeCommand("quarto.test_setkVisualModeConfirmedTrue"); + await vscode.commands.executeCommand( + "vscode.openWith", + uri, + VisualEditorProvider.viewType + ); + + await openAndShowExamplesTextDocument("hello.lua", { viewColumn: vscode.ViewColumn.Beside, preserveFocus: false }); + + // TODO: Could we hook into an event instead? Currently really hard because of the + // issue noted above. + await wait(APPROX_TIME_TO_OPEN_VISUAL_EDITOR); + + const quartoEditor = findQuartoEditorWithDefaults(); + + assertQuartoEditorForVisualEditor(quartoEditor, uri, vscode.ViewColumn.One); + }); + + test('finds the active .ipynb notebook editor', async function () { + this.timeout(30_000); // Opening the first notebook can be slow, especially on CI + + console.log('open notebook 1'); + const notebook = await vscode.workspace.openNotebookDocument(examplesUri("convert-ipynb-to-qmd.ipynb")); + console.log('show notebook 1'); + const notebookEditor = await vscode.window.showNotebookDocument(notebook); + console.log('open notebook 2'); + const notebook2 = await vscode.workspace.openNotebookDocument(examplesUri("hello.ipynb")); + console.log('show notebook 2'); + await vscode.window.showNotebookDocument(notebook2, { viewColumn: vscode.ViewColumn.Beside, preserveFocus: true }); + + console.log('find quarto editor'); + const quartoEditor = findQuartoEditorWithDefaults(); + + console.log('assert quarto editor'); + assertQuartoEditorForNotebookEditor(quartoEditor, notebookEditor); + }); + + test('finds a visible (inactive) .ipynb notebook editor', async function () { + const notebook = await vscode.workspace.openNotebookDocument(examplesUri("convert-ipynb-to-qmd.ipynb")); + const notebookEditor = await vscode.window.showNotebookDocument(notebook); + await openAndShowExamplesTextDocument("hello.lua", { viewColumn: vscode.ViewColumn.Beside, preserveFocus: false }); + + const quartoEditor = findQuartoEditorWithDefaults(); + + assertQuartoEditorForNotebookEditor(quartoEditor, notebookEditor); + }); + }); +}); + +/** Convenience helper wrapping `findQuartoEditor` with defaults. */ +function findQuartoEditorWithDefaults() { + return findQuartoEditor( + new MarkdownEngine(), + initQuartoContext(), + canPreviewDoc + ); +} + +function assertQuartoEditorForTextEditor( + quartoEditor: QuartoEditor | undefined, + textEditor: vscode.TextEditor, +) { + assert.ok( + quartoEditor, + "Expected to find a Quarto editor for the active text editor" + ); + assert.strictEqual( + quartoEditor.document, + textEditor.document, + "Expected the found editor's document to be the active document" + ); + assert.strictEqual( + quartoEditor.notebook, + undefined, + "Expected notebook to be undefined for a text editor" + ); + assert.strictEqual( + quartoEditor.textEditor, + textEditor, + "Expected the found editor's text editor to be the active text editor" + ); + assert.strictEqual( + quartoEditor.viewColumn, + textEditor.viewColumn, + "Expected the found editor's view column to match the active text editor's view column" + ); +} + +function assertQuartoEditorForVisualEditor( + quartoEditor: QuartoEditor | undefined, + expectedUri: vscode.Uri, + expectedViewColumn: vscode.ViewColumn, +) { + assert.ok( + quartoEditor, + "Expected to find a Quarto editor for the active visual editor" + ); + assert.strictEqual( + quartoEditor.document.uri.toString(), + expectedUri.toString(), + "Expected the found editor's document to be the active visual editor's document" + ); + assert.strictEqual( + quartoEditor.textEditor, + undefined, + "Expected text editor to be undefined for a visual editor" + ); + assert.strictEqual( + quartoEditor.notebook, + undefined, + "Expected notebook to be undefined for a visual editor" + ); + assert.strictEqual( + quartoEditor.viewColumn, + expectedViewColumn, + "Expected the found editor's view column to be the active visual editor's view column" + ); +} + +function assertQuartoEditorForNotebookEditor( + quartoEditor: QuartoEditor | undefined, + notebookEditor: vscode.NotebookEditor, +) { + assert.ok( + quartoEditor, + "Expected to find a Quarto editor for the active notebook editor" + ); + + assert.ok( + notebookEditor.notebook.getCells().map(c => c.document).includes(quartoEditor.document), + "Expected the found editor's document to be a cell in the active notebook editor" + ); + assert.strictEqual( + quartoEditor.notebook, + notebookEditor.notebook, + "Expected the found editor's notebook to be the active notebook" + ); +} diff --git a/apps/vscode/src/test/test-utils.ts b/apps/vscode/src/test/test-utils.ts index 0fd78e20..3fc0918c 100644 --- a/apps/vscode/src/test/test-utils.ts +++ b/apps/vscode/src/test/test-utils.ts @@ -27,17 +27,26 @@ export function wait(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } -export async function openAndShowExamplesTextDocument(fileName: string) { - return openAndShowUri(examplesUri(fileName)); +export async function openAndShowExamplesTextDocument( + fileName: string, + showOptions?: vscode.TextDocumentShowOptions +) { + return openAndShowUri(examplesUri(fileName), showOptions); } -export async function openAndShowExamplesOutTextDocument(fileName: string) { - return openAndShowUri(examplesOutUri(fileName)); +export async function openAndShowExamplesOutTextDocument( + fileName: string, + showOptions?: vscode.TextDocumentShowOptions +) { + return openAndShowUri(examplesOutUri(fileName), showOptions); } -export async function openAndShowUri(uri: vscode.Uri) { +export async function openAndShowUri( + uri: vscode.Uri, + showOptions?: vscode.TextDocumentShowOptions +) { const doc = await vscode.workspace.openTextDocument(uri); - const editor = await vscode.window.showTextDocument(doc); + const editor = await vscode.window.showTextDocument(doc, showOptions); return { doc, editor }; } @@ -73,7 +82,7 @@ export async function openUniqueExampleDocument(fileName: string) { }; } -const APPROX_TIME_TO_OPEN_VISUAL_EDITOR = 1700; +export const APPROX_TIME_TO_OPEN_VISUAL_EDITOR = 1700; export async function roundtrip(doc: vscode.TextDocument) { const before = doc.getText(); From 56f786b8f41641c68ab1536ec2417832b3e5e88d Mon Sep 17 00:00:00 2001 From: seem Date: Thu, 18 Jun 2026 09:46:33 +0200 Subject: [PATCH 4/5] rebase changelog --- apps/vscode/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/vscode/CHANGELOG.md b/apps/vscode/CHANGELOG.md index 95fcce61..6c74d4c6 100644 --- a/apps/vscode/CHANGELOG.md +++ b/apps/vscode/CHANGELOG.md @@ -3,11 +3,11 @@ ## 1.134.0 (Unreleased) - The "Preview" and "Preview Format..." commands now show in the Positron Notebook Editor overflow menu (). -- Fixed notebooks failing to render or preview when they were visible but not active, or while the Panel or Sidebar were open (). - In Positron, Jupyter Notebooks (`.ipynb`) are now exported via the new unified "Export" command, rather than the "Quarto: Convert to .qmd" command (). - Fixed a bug where formatting a code cell stripped leading empty lines. Leading empty lines between option directives and code are now preserved, and two or more leading empty lines are collapsed to one (). - Fixed a bug where IPython magics (`%`, `%%`) and shell escapes (`!`) in Python code cells produced spurious diagnostics from language servers like Pyrefly and Ruff. These lines are now commented out in the virtual document handed to language servers (). - The "Render Document" command is now available in the Positron Notebook Editor (). +- Fixed notebooks failing to render or preview when they were visible but not active, or while the Panel or Sidebar were open (). ## 1.133.0 (Release on 2026-06-03) From 9630827e869b49b0a86174dc6a29a0c93db75420 Mon Sep 17 00:00:00 2001 From: seem Date: Thu, 18 Jun 2026 09:53:49 +0200 Subject: [PATCH 5/5] note why we use the first cell doc --- apps/vscode/src/core/doc.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/vscode/src/core/doc.ts b/apps/vscode/src/core/doc.ts index 81e04295..f8459c4a 100644 --- a/apps/vscode/src/core/doc.ts +++ b/apps/vscode/src/core/doc.ts @@ -243,6 +243,16 @@ export function findQuartoEditor( filter(editor.notebook.cellAt(0)?.document) ); if (visibleNotebookEditor) { + // NOTE: We used to get the text document belonging to the first item in + // `vscode.window.visibleTextEditors` whose URI matched the notebook. + // However, there was a bug where cells would stop appearing in + // `visibleTextEditors` when the Panel or Sidebar were open. Now we + // arbitrarily use the first cell's document. This is ok because, + // for notebooks, the rest of this extension only requires that + // `QuartoEditor.document` belongs to any cell in the notebook. + // A better longer-term solution would be to *not* require a text + // document for notebook `QuartoEditor`s and expose the required + // information (e.g. `document.uri`) in another way. const firstCellDocument = visibleNotebookEditor.notebook.cellAt(0).document; return quartoNotebookEditor(visibleNotebookEditor, firstCellDocument); }