From 9084df0131c0dbed78fb756b0de3344a85601951 Mon Sep 17 00:00:00 2001 From: "Jennifer (Jenny) Bryan" Date: Sun, 24 Nov 2024 21:50:39 -0800 Subject: [PATCH] (Start to) handle cli hyperlinks in the terminal used for pkg dev tasks (#5231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses #5218 This PR makes suggested code hints like `testthat::snapshot_accept()` or `testthat::snapshot_review()` clickable and runnable from the integrated terminal where positron-r sends package development tasks, such as `devtools::test()`. "Runnable" in the sense that the code is run in the user's R console. This gets routed into the same handlers as clickable code entered directly in the console, so the existing safety guards are in place. Screenshot 2024-11-18 at 1 04 51 PM (Note that this PR does _not_ change behaviour for file hyperlinks, which I've crossed out in the screenshot above. Those hyperlinks currently get delegated to the operating system, so for many R users, a `.R` file will open in RStudio. Changing that is a separate problem #5409.) ### QA Notes To see the new behaviour, you need a development version of the cli R package. I would install that via `pak::pak("r-lib/cli")`. (Context: https://github.com/r-lib/cli/pull/739.) Then you need to run tests or `R CMD check` on a package with a failing snapshot test, in order to get a clickable invitation to accept a new snapshot or to review the proposed snapshot in a Shiny app. I have made a toy package that could be obtained via [`usethis::create_from_github("jennybc/clilinks")`](https://github.com/jennybc/clilinks). It has a couple of snapshot tests that will always fail 😄 because they attempt to snapshot a random number. * Install dev cli: `pak::pak("r-lib/cli")` * Identify a package that tickles this feature and open it in Positron, perhaps via: `usethis::create_from_github("jennybc/clilinks")` * Use our gesture for running package tests: Ctrl/Cmd + Shift + T or select *R: Test R Package* from the command palette. * Click on a "runnable code" hyperlink: - Note this requires Ctrl/Cmd click! This is a VS Code convention, not specific to us. - Note that you might have to click twice. This is a bug (or 2 bugs?) for which fixes are making their way through the system (https://github.com/microsoft/vscode/issues/230010). Again, not us. - You will need to grant the extension permission to handle such URIs, either as a one-off or more persistently. This is a VS Code feature. Screenshot 2024-11-18 at 1 04 51 PM Screenshot 2024-11-18 at 12 55 37 PM If you click `testthat::snapshot_review()`, you should see the code execute in the R console and this Shiny app appears. Click on "Accept" (the fact that "Skip" seems broken is https://github.com/r-lib/testthat/issues/2025 and unrelated to this PR). Screenshot 2024-11-18 at 12 55 47 PM --- extensions/positron-r/package.json | 8 +- extensions/positron-r/package.nls.json | 3 +- extensions/positron-r/src/extension.ts | 4 + extensions/positron-r/src/session.ts | 2 +- extensions/positron-r/src/tasks.ts | 8 +- extensions/positron-r/src/uri-handler.ts | 99 +++++++++++++++++++ extensions/positron-reticulate/package.json | 2 +- .../common/positronNewProjectService.ts | 6 +- 8 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 extensions/positron-r/src/uri-handler.ts diff --git a/extensions/positron-r/package.json b/extensions/positron-r/package.json index 12ba9e40e53..bd6a7871c4e 100644 --- a/extensions/positron-r/package.json +++ b/extensions/positron-r/package.json @@ -3,7 +3,7 @@ "displayName": "%displayName%", "description": "%description%", "version": "0.0.2", - "publisher": "vscode", + "publisher": "positron", "engines": { "vscode": "^1.65.0" }, @@ -281,6 +281,12 @@ }, "default": [], "description": "%r.configuration.extraArguments.description%" + }, + "positron.r.taskHyperlinks": { + "scope": "window", + "type": "boolean", + "default": false, + "description": "%r.configuration.taskHyperlinks.description%" } } } diff --git a/extensions/positron-r/package.nls.json b/extensions/positron-r/package.nls.json index 3f5ee6eb8ab..6a12749ba20 100644 --- a/extensions/positron-r/package.nls.json +++ b/extensions/positron-r/package.nls.json @@ -58,5 +58,6 @@ "r.configuration.pipe.magrittr.token": "%>%", "r.configuration.pipe.native.description": "Native pipe available in R >= 4.1", "r.configuration.pipe.magrittr.description": "Pipe operator from the magrittr package, re-exported by many other packages", - "r.configuration.diagnostics.enable.description": "Enable R diagnostics globally" + "r.configuration.diagnostics.enable.description": "Enable R diagnostics globally", + "r.configuration.taskHyperlinks.description": "Turn on experimental support for hyperlinks in package development tasks" } diff --git a/extensions/positron-r/src/extension.ts b/extensions/positron-r/src/extension.ts index 7cb8aec93af..26963c07caf 100644 --- a/extensions/positron-r/src/extension.ts +++ b/extensions/positron-r/src/extension.ts @@ -12,6 +12,7 @@ import { providePackageTasks } from './tasks'; import { setContexts } from './contexts'; import { setupTestExplorer, refreshTestExplorer } from './testing/testing'; import { RRuntimeManager } from './runtime-manager'; +import { registerUriHandler } from './uri-handler'; export const LOGGER = vscode.window.createOutputChannel('Positron R Extension', { log: true }); @@ -36,6 +37,9 @@ export function activate(context: vscode.ExtensionContext) { // Provide tasks. providePackageTasks(context); + // Prepare to handle cli-produced hyperlinks that target the positron-r extension. + registerUriHandler(); + // Setup testthat test explorer. setupTestExplorer(context); vscode.workspace.onDidChangeConfiguration(async event => { diff --git a/extensions/positron-r/src/session.ts b/extensions/positron-r/src/session.ts index b6fa660496f..7fb50748838 100644 --- a/extensions/positron-r/src/session.ts +++ b/extensions/positron-r/src/session.ts @@ -27,7 +27,7 @@ interface RPackageInstallation { compatible: boolean; } -interface EnvVar { +export interface EnvVar { [key: string]: string; } diff --git a/extensions/positron-r/src/tasks.ts b/extensions/positron-r/src/tasks.ts index 8a49ae825ab..9f62b844479 100644 --- a/extensions/positron-r/src/tasks.ts +++ b/extensions/positron-r/src/tasks.ts @@ -8,6 +8,7 @@ import * as os from 'os'; import { RSessionManager } from './session-manager'; import { getPandocPath } from './pandoc'; import { getEnvVars } from './session'; +import { prepCliEnvVars } from './uri-handler'; export class RPackageTaskProvider implements vscode.TaskProvider { @@ -39,7 +40,7 @@ export async function getRPackageTasks(editorFilePath?: string): Promise('taskHyperlinks'); + + return taskHyperlinksEnabled === true; +} + +// Example of a URI we expect to handle: +// positron://positron.positron-r/cli?command=x-r-run:testthat::snapshot_review('snap') +// +// How the example URI breaks down: +// { +// "scheme": "positron", +// "authority": "positron.positron-r", +// "path": "/cli", +// "query": "command=x-r-run:testthat::snapshot_review('zzz')", +// "fragment": "", +// "fsPath": "/cli" +// } +function handleUri(uri: vscode.Uri): void { + if (uri.path !== '/cli') { + return; + } + + // Turns this query string + // "command=x-r-run:testthat::snapshot_review('zzz')" + // into this object + // { "command": "x-r-run:testthat::snapshot_review('zzz')" } + const query = new URLSearchParams(uri.query); + const command = query.get('command'); + if (!command) { + return; + } + + const commandRegex = /^(x-r-(help|run|vignette)):(.+)$/; + if (!commandRegex.test(command)) { + return; + } + + const session = RSessionManager.instance.getConsoleSession(); + if (!session) { + return; + } + + session.openResource(command); + vscode.commands.executeCommand('workbench.panel.positronConsole.focus'); +} + +export async function prepCliEnvVars(session?: RSession): Promise { + session = session || RSessionManager.instance.getConsoleSession(); + if (!session) { + return {}; + } + + const taskHyperlinks = taskHyperlinksEnabled(); + const cliPkg = await session.packageVersion('cli', '3.6.3.9001'); + const cliSupportsHyperlinks = cliPkg?.compatible ?? false; + + if (!taskHyperlinks || !cliSupportsHyperlinks) { + // eslint-disable-next-line @typescript-eslint/naming-convention + return { R_CLI_HYPERLINKS: 'FALSE' }; + } + + return { + /* eslint-disable @typescript-eslint/naming-convention */ + R_CLI_HYPERLINKS: 'TRUE', + // TODO: I'd like to request POSIX compliant hyperlinks in the future, but currently + // cli's tests implicitly assume the default and there are more important changes to + // propose in cli, such as tweaks to file hyperlinks. Leave this alone for now. + // R_CLI_HYPERLINK_MODE: "posix", + R_CLI_HYPERLINK_RUN: 'TRUE', + R_CLI_HYPERLINK_RUN_URL_FORMAT: 'positron://positron.positron-r/cli?command=x-r-run:{code}', + R_CLI_HYPERLINK_HELP: 'TRUE', + R_CLI_HYPERLINK_HELP_URL_FORMAT: 'positron://positron.positron-r/cli?command=x-r-help:{topic}', + R_CLI_HYPERLINK_VIGNETTE: 'TRUE', + R_CLI_HYPERLINK_VIGNETTE_URL_FORMAT: 'positron://positron.positron-r/cli?command=x-r-vignette:{vignette}' + /* eslint-enable @typescript-eslint/naming-convention */ + }; +} diff --git a/extensions/positron-reticulate/package.json b/extensions/positron-reticulate/package.json index 4f22abe4d53..5567a353038 100644 --- a/extensions/positron-reticulate/package.json +++ b/extensions/positron-reticulate/package.json @@ -36,7 +36,7 @@ }, "extensionDependencies": [ "ms-python.python", - "vscode.positron-r", + "positron.positron-r", "vscode.jupyter-adapter" ], "devDependencies": { diff --git a/src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts b/src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts index c61d1fa49c0..20d13d2ebf8 100644 --- a/src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts +++ b/src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts @@ -283,7 +283,7 @@ export class PositronNewProjectService extends Disposable implements IPositronNe /** * Runs R Project specific tasks. - * Relies on extension vscode.positron-r + * Relies on extension positron.positron-r */ private async _runRTasks() { // no-op for now, since we haven't defined any pre-runtime startup R tasks @@ -305,7 +305,7 @@ export class PositronNewProjectService extends Disposable implements IPositronNe /** * Runs R Project specific post-initialization tasks. - * Relies on extension vscode.positron-r + * Relies on extension positron.positron-r */ private async _runRPostInitTasks() { // Create the R environment @@ -518,7 +518,7 @@ export class PositronNewProjectService extends Disposable implements IPositronNe /** * Creates the R environment. - * Relies on extension vscode.positron-r + * Relies on extension positron.positron-r */ private async _createREnvironment() { if (this._newProjectConfig?.useRenv) {