diff --git a/apps/vscode/src/@types/hooks.d.ts b/apps/vscode/src/@types/hooks.d.ts index f72cb390..f971d084 100644 --- a/apps/vscode/src/@types/hooks.d.ts +++ b/apps/vscode/src/@types/hooks.d.ts @@ -8,6 +8,7 @@ declare module 'positron' { export interface PositronApi { version: string; runtime: PositronRuntime; + languages: PositronLanguages; window: PositronWindow; } @@ -19,6 +20,26 @@ declare module 'positron' { ): Thenable; } + export interface PositronLanguages { + registerStatementRangeProvider( + selector: vscode.DocumentSelector, + provider: StatementRangeProvider + ): vscode.Disposable; + } + + export interface StatementRangeProvider { + provideStatementRange( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken + ): vscode.ProviderResult; + } + + export interface StatementRange { + readonly range: vscode.Range; + readonly code?: string; + } + export interface PositronWindow { createPreviewPanel( viewType: string, diff --git a/apps/vscode/src/host/executors.ts b/apps/vscode/src/host/executors.ts index 29eecc26..7f749f63 100644 --- a/apps/vscode/src/host/executors.ts +++ b/apps/vscode/src/host/executors.ts @@ -75,7 +75,7 @@ const jupyterCellExecutor = (language: string) : VSCodeCellExecutor => ({ await commands.executeCommand("jupyter.execSelectionInteractive", code); } }, -}) +}); const pythonCellExecutor = jupyterCellExecutor("python"); diff --git a/apps/vscode/src/host/hooks.ts b/apps/vscode/src/host/hooks.ts index 76431c80..6750cc82 100644 --- a/apps/vscode/src/host/hooks.ts +++ b/apps/vscode/src/host/hooks.ts @@ -13,14 +13,13 @@ * */ -import { Uri, WebviewPanelOptions, WebviewOptions, ViewColumn } from 'vscode'; - +import * as vscode from 'vscode'; import * as hooks from 'positron'; -import { ExtensionHost, HostWebviewPanel } from '.'; +import { ExtensionHost, HostWebviewPanel, HostStatementRangeProvider } from '.'; import { CellExecutor, cellExecutorForLanguage, executableLanguages, isKnitrDocument, pythonWithReticulate } from './executors'; -import { TextDocument } from 'vscode'; import { MarkdownEngine } from '../markdown/engine'; +import { virtualDoc, virtualDocUri, adjustedPosition } from "../vdoc/vdoc"; declare global { function acquirePositronApi() : hooks.PositronApi; @@ -49,14 +48,14 @@ export function hooksExtensionHost() : ExtensionHost { // w/o runtimes so we support all languages) executableLanguages, - cellExecutorForLanguage: async (language: string, document: TextDocument, engine: MarkdownEngine, silent?: boolean) + cellExecutorForLanguage: async (language: string, document: vscode.TextDocument, engine: MarkdownEngine, silent?: boolean) : Promise => { switch(language) { // use hooks for known runtimes case "python": case "r": return { - execute: async (blocks: string[], _editorUri?: Uri) : Promise => { + execute: async (blocks: string[], _editorUri?: vscode.Uri) : Promise => { for (const block of blocks) { let code = block; if (language === "python" && isKnitrDocument(document, engine)) { @@ -65,7 +64,10 @@ export function hooksExtensionHost() : ExtensionHost { } await hooksApi()?.runtime.executeCode(language, code, false); } - } + }, + executeSelection: async () : Promise => { + await vscode.commands.executeCommand('workbench.action.positronConsole.executeCode', {languageId: language}); + } }; // delegate for other languages @@ -74,11 +76,20 @@ export function hooksExtensionHost() : ExtensionHost { } }, + registerStatementRangeProvider: (engine: MarkdownEngine): vscode.Disposable => { + const hooks = hooksApi(); + if (hooks) { + return hooks.languages.registerStatementRangeProvider('quarto', + new EmbeddedStatementRangeProvider(engine)); + } + return new vscode.Disposable(() => {}); + }, + createPreviewPanel: ( viewType: string, title: string, preserveFocus?: boolean, - options?: WebviewPanelOptions & WebviewOptions + options?: vscode.WebviewPanelOptions & vscode.WebviewOptions ): HostWebviewPanel => { // create preview panel @@ -106,10 +117,53 @@ class HookWebviewPanel implements HostWebviewPanel { get webview() { return this.panel_.webview; }; get visible() { return this.panel_.visible; }; - reveal(_viewColumn?: ViewColumn, preserveFocus?: boolean) { + reveal(_viewColumn?: vscode.ViewColumn, preserveFocus?: boolean) { this.panel_.reveal(preserveFocus); } onDidChangeViewState = this.panel_.onDidChangeViewState; onDidDispose = this.panel_.onDidDispose; dispose() { this.panel_.dispose(); }; -} \ No newline at end of file +} + +class EmbeddedStatementRangeProvider implements HostStatementRangeProvider { + private readonly _engine: MarkdownEngine; + + constructor( + readonly engine: MarkdownEngine, + ) { + this._engine = engine; + } + + async provideStatementRange( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken): Promise { + const vdoc = await virtualDoc(document, position, this._engine); + if (vdoc) { + const vdocUri = await virtualDocUri(vdoc, document.uri, "statementRange"); + try { + return getStatementRange(vdocUri.uri, adjustedPosition(vdoc.language, position)); + } catch (error) { + return undefined; + } finally { + if (vdocUri.cleanup) { + await vdocUri.cleanup(); + } + } + } else { + return undefined; + } + }; +} + +async function getStatementRange( + uri: vscode.Uri, + position: vscode.Position, +) { + return await vscode.commands.executeCommand( + "vscode.executeStatementRangeProvider", + uri, + position + ); +} + diff --git a/apps/vscode/src/host/index.ts b/apps/vscode/src/host/index.ts index fb64d5a9..951e8d4d 100644 --- a/apps/vscode/src/host/index.ts +++ b/apps/vscode/src/host/index.ts @@ -34,6 +34,19 @@ export interface HostWebviewPanel extends vscode.Disposable { readonly onDidDispose: vscode.Event; } +export interface HostStatementRangeProvider { + provideStatementRange( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken + ): vscode.ProviderResult; +} + +export interface HostStatementRange { + readonly range: vscode.Range; + readonly code?: string; +} + export interface ExtensionHost { // code execution @@ -45,6 +58,11 @@ export interface ExtensionHost { silent?: boolean ) : Promise; + // statement range provider + registerStatementRangeProvider( + engine: MarkdownEngine, + ): vscode.Disposable; + // preview createPreviewPanel( viewType: string, @@ -73,14 +91,18 @@ function defaultExtensionHost() : ExtensionHost { return { executableLanguages: (visualMode: boolean, document: TextDocument, engine: MarkdownEngine) => { - const languages = executableLanguages(); - const knitr = isKnitrDocument(document, engine); + const languages = executableLanguages(); + const knitr = isKnitrDocument(document, engine); - // jupyter python (as distinct from knitr python) doesn't work in visual mode b/c - // jupyter.execSelectionInteractive wants a text editor to be active - return languages.filter(language => knitr || !visualMode || (language !== "python")); + // jupyter python (as distinct from knitr python) doesn't work in visual mode b/c + // jupyter.execSelectionInteractive wants a text editor to be active + return languages.filter(language => knitr || !visualMode || (language !== "python")); }, cellExecutorForLanguage, + // in the default extension host, this is a noop: + registerStatementRangeProvider: (engine: MarkdownEngine): vscode.Disposable => { + return new vscode.Disposable(() => {}); + }, createPreviewPanel, }; } \ No newline at end of file diff --git a/apps/vscode/src/lsp/client.ts b/apps/vscode/src/lsp/client.ts index 5f3a9858..4dc7d22c 100644 --- a/apps/vscode/src/lsp/client.ts +++ b/apps/vscode/src/lsp/client.ts @@ -63,6 +63,7 @@ import { import { getHover, getSignatureHelpHover } from "../core/hover"; import { imageHover } from "../providers/hover-image"; import { LspInitializationOptions, QuartoContext } from "quarto-core"; +import { extensionHost } from "../host"; let client: LanguageClient; @@ -108,7 +109,8 @@ export async function activateLsp( if (config.get("cells.signatureHelp.enabled", true)) { middleware.provideSignatureHelp = embeddedSignatureHelpProvider(engine); } - + extensionHost().registerStatementRangeProvider(engine); + // create client options const initializationOptions : LspInitializationOptions = { quartoBinPath: quartoContext.binPath diff --git a/apps/vscode/src/vdoc/vdoc.ts b/apps/vscode/src/vdoc/vdoc.ts index 9845551b..d673758e 100644 --- a/apps/vscode/src/vdoc/vdoc.ts +++ b/apps/vscode/src/vdoc/vdoc.ts @@ -116,7 +116,8 @@ export type VirtualDocAction = "hover" | "signature" | "definition" | - "format"; + "format" | + "statementRange"; export type VirtualDocUri = { uri: Uri, cleanup?: () => Promise };