diff --git a/apps/vscode/package.json b/apps/vscode/package.json index e1a9b804..78c0d9d3 100644 --- a/apps/vscode/package.json +++ b/apps/vscode/package.json @@ -302,7 +302,7 @@ "category": "Quarto", "actionBarOptions": { "controlType": "button", - "displayTitle": true + "displayTitle": false } }, { diff --git a/apps/vscode/src/@types/hooks.d.ts b/apps/vscode/src/@types/hooks.d.ts index 16a248fd..32bf8a12 100644 --- a/apps/vscode/src/@types/hooks.d.ts +++ b/apps/vscode/src/@types/hooks.d.ts @@ -19,6 +19,11 @@ declare module 'positron' { focus: boolean, allowIncomplete: boolean ): Thenable; + + executeInlineCell( + documentUri: vscode.Uri, + cellRanges: vscode.Range[] + ): Thenable; } export interface PositronLanguages { diff --git a/apps/vscode/src/host/executors.ts b/apps/vscode/src/host/executors.ts index 96035dab..f30e18c5 100644 --- a/apps/vscode/src/host/executors.ts +++ b/apps/vscode/src/host/executors.ts @@ -13,7 +13,7 @@ * */ -import { Uri, commands, window, extensions } from "vscode"; +import { Uri, commands, window, extensions, Range } from "vscode"; import semver from "semver"; import { TextDocument } from "vscode"; @@ -28,6 +28,7 @@ export interface CellExecutor { execute: (blocks: string[], editorUri?: Uri) => Promise; executeSelection?: () => Promise; executeAtPosition?: (uri: Uri, pos: Position) => Promise; + executeInlineCells?: (documentUri: Uri, cellRanges: Range[]) => Promise; } export function executableLanguages() { diff --git a/apps/vscode/src/host/hooks.ts b/apps/vscode/src/host/hooks.ts index fc04f17d..647521cb 100644 --- a/apps/vscode/src/host/hooks.ts +++ b/apps/vscode/src/host/hooks.ts @@ -23,9 +23,19 @@ import { CellExecutor, cellExecutorForLanguage, executableLanguages, isKnitrDocu import { ExecuteQueue } from './execute-queue'; import { MarkdownEngine } from '../markdown/engine'; import { virtualDoc, adjustedPosition, unadjustedRange, withVirtualDocUri } from "../vdoc/vdoc"; -import { Position } from 'vscode'; +import { Position, Range } from 'vscode'; import { Uri } from 'vscode'; +/** + * Check if inline output is enabled in Positron settings. + * This helper is shared with main.ts for code lens visibility. + */ +export function isInlineOutputEnabled(): boolean { + return vscode.workspace + .getConfiguration("positron.quarto.inlineOutput") + .get("enabled", false); +} + declare global { function acquirePositronApi(): hooks.PositronApi; } @@ -61,7 +71,7 @@ export function hooksExtensionHost(): ExtensionHost { case "csharp": case "r": return { - execute: async (blocks: string[], _editorUri?: vscode.Uri): Promise => { + execute: async (blocks: string[], editorUri?: vscode.Uri): Promise => { const runtime = hooksApi()?.runtime; if (runtime === undefined) { @@ -99,6 +109,16 @@ export function hooksExtensionHost(): ExtensionHost { console.error('error when using `positron.executeCodeFromPosition`'); } return position; + }, + executeInlineCells: async (documentUri: vscode.Uri, cellRanges: Range[]): Promise => { + const runtime = hooksApi()?.runtime; + + if (runtime === undefined) { + // Can't do anything without a runtime + return; + } + + await runtime.executeInlineCell(documentUri, cellRanges); } }; diff --git a/apps/vscode/src/main.ts b/apps/vscode/src/main.ts index 7f170f5e..54277544 100644 --- a/apps/vscode/src/main.ts +++ b/apps/vscode/src/main.ts @@ -32,6 +32,7 @@ import { activateEditor } from "./providers/editor/editor"; import { activateCopyFiles } from "./providers/copyfiles"; import { activateZotero } from "./providers/zotero/zotero"; import { extensionHost } from "./host"; +import { isInlineOutputEnabled } from "./host/hooks"; import { initQuartoContext, getSourceDescription } from "quarto-core"; import { configuredQuartoPath } from "./core/quarto"; import { activateDenoConfig } from "./providers/deno-config"; @@ -153,11 +154,47 @@ export async function activate(context: vscode.ExtensionContext): Promise { + const inlineOutputEnabled = isInlineOutputEnabled(); + + if (inlineOutputEnabled && codeLensDisposable) { + // Dispose existing code lens when inline output is enabled + codeLensDisposable.dispose(); + codeLensDisposable = undefined; + } else if (!inlineOutputEnabled && !codeLensDisposable) { + // Register code lens when inline output is disabled + codeLensDisposable = vscode.languages.registerCodeLensProvider( + kQuartoDocSelector, + quartoCellExecuteCodeLensProvider(host, engine) + ); + context.subscriptions.push(codeLensDisposable); + } + }; + + // Initial setup + updateCodeLens(); + + // Listen for setting changes + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration("positron.quarto.inlineOutput.enabled")) { + updateCodeLens(); + } + }) + ); + } else { + // In VS Code, always register the code lens + vscode.languages.registerCodeLensProvider( + kQuartoDocSelector, + quartoCellExecuteCodeLensProvider(host, engine) + ); + } // provide file copy/drop handling activateCopyFiles(context); diff --git a/apps/vscode/src/providers/cell/commands.ts b/apps/vscode/src/providers/cell/commands.ts index 0a042d68..8f07dff9 100644 --- a/apps/vscode/src/providers/cell/commands.ts +++ b/apps/vscode/src/providers/cell/commands.ts @@ -176,7 +176,8 @@ class RunCurrentCellCommand extends RunCommand implements Command { const executor = await this.cellExecutorForLanguage(language, editor.document, this.engine_); if (executor) { const code = codeWithoutOptionsFromBlock(block); - await executeInteractive(executor, [code], editor.document); + const ranges = block.range ? [block.range] : undefined; + await executeInteractive(executor, [code], editor.document, ranges); } } } @@ -499,16 +500,21 @@ class RunCellsAboveCommand extends RunCommand implements Command { const executor = await this.cellExecutorForLanguage(language, editor.document, this.engine_); if (executor) { - // accumulate code + // accumulate code and ranges const code: string[] = []; - for (const block of blocks.filter( + const ranges: Range[] = []; + for (const blk of blocks.filter( isExecutableLanguageBlockOf(language) - )) { - code.push(codeWithoutOptionsFromBlock(block)); + ) as Array) { + code.push(codeWithoutOptionsFromBlock(blk)); + if (blk.range) { + ranges.push(blk.range); + } } - // execute - await executeInteractive(executor, code, editor.document); + // execute (only pass ranges if we collected the same number as code blocks) + const validRanges = ranges.length === code.length ? ranges : undefined; + await executeInteractive(executor, code, editor.document, validRanges); } } } @@ -558,9 +564,10 @@ class RunCellsBelowCommand extends RunCommand implements Command { : undefined; const blocks: string[] = []; + const ranges: Range[] = []; for (const blk of tokens.filter((token?: Token) => blockIsExecutable(this.host_, token)) as Array) { // skip if the cell is above or at the cursor - if (line < blk.range.start.line) { + if (blk.range && line < blk.range.start.line) { // set language if needed const blockLanguage = languageNameFromBlock(blk); if (!language) { @@ -569,14 +576,16 @@ class RunCellsBelowCommand extends RunCommand implements Command { // include blocks of this language if (blockLanguage === language) { blocks.push(codeWithoutOptionsFromBlock(blk)); + ranges.push(blk.range); } } } - // execute + // execute (only pass ranges if we collected the same number as code blocks) if (language && blocks.length > 0) { const executor = await this.cellExecutorForLanguage(language, editor.document, this.engine_); if (executor) { - await executeInteractive(executor, blocks, editor.document); + const validRanges = ranges.length === blocks.length ? ranges : undefined; + await executeInteractive(executor, blocks, editor.document, validRanges); } } } @@ -622,19 +631,25 @@ class RunAllCellsCommand extends RunCommand implements Command { ) { let language: string | undefined; const blocks: string[] = []; - for (const block of tokens.filter((token?: Token) => blockIsExecutable(this.host_, token)) as Array) { - const blockLanguage = languageNameFromBlock(block); + const ranges: Range[] = []; + for (const blk of tokens.filter((token?: Token) => blockIsExecutable(this.host_, token)) as Array) { + const blockLanguage = languageNameFromBlock(blk); if (!language) { language = blockLanguage; } if (blockLanguage === language) { - blocks.push(codeWithoutOptionsFromBlock(block)); + blocks.push(codeWithoutOptionsFromBlock(blk)); + if (blk.range) { + ranges.push(blk.range); + } } } if (language && blocks.length > 0) { const executor = await this.cellExecutorForLanguage(language, editor.document, this.engine_); if (executor) { - await executeInteractive(executor, blocks, editor.document); + // only pass ranges if we collected the same number as code blocks + const validRanges = ranges.length === blocks.length ? ranges : undefined; + await executeInteractive(executor, blocks, editor.document, validRanges); } } } @@ -727,7 +742,8 @@ async function runAdjacentBlock(host: ExtensionHost, editor: TextEditor, engine: const language = languageNameFromBlock(block); const executor = await host.cellExecutorForLanguage(language, editor.document, engine); if (executor) { - await executeInteractive(executor, [codeWithoutOptionsFromBlock(block)], editor.document); + const ranges = block.range ? [block.range] : undefined; + await executeInteractive(executor, [codeWithoutOptionsFromBlock(block)], editor.document, ranges); } } diff --git a/apps/vscode/src/providers/cell/executors.ts b/apps/vscode/src/providers/cell/executors.ts index 9f28fab9..918a86fd 100644 --- a/apps/vscode/src/providers/cell/executors.ts +++ b/apps/vscode/src/providers/cell/executors.ts @@ -17,7 +17,7 @@ // (e.g. see https://github.com/JeepShen/vscode-markdown-code-runner) -import { TextDocument } from "vscode"; +import { TextDocument, Range } from "vscode"; import { codeForExecutableLanguageBlock, @@ -34,6 +34,7 @@ import { cellOptionsForToken, kExecuteEval } from "./options"; import { CellExecutor, ExtensionHost } from "../../host"; import { executableLanguages } from "../../host/executors"; +import { isInlineOutputEnabled } from "../../host/hooks"; import { Position } from "vscode"; import { Uri } from "vscode"; @@ -87,8 +88,18 @@ export function codeWithoutOptionsFromBlock(token: TokenMath | TokenCodeBlock) { export async function executeInteractive( executor: CellExecutor, blocks: string[], - document: TextDocument + document: TextDocument, + ranges?: Range[] ): Promise { + // If inline output is enabled, the document has a URI, and the executor supports + // inline execution, use that instead of the standard console execution + if (isInlineOutputEnabled() && + !document.isUntitled && + ranges && + ranges.length > 0 && + executor.executeInlineCells) { + return await executor.executeInlineCells(document.uri, ranges); + } return await executor.execute(blocks, !document.isUntitled ? document.uri : undefined); }