Skip to content

Commit

Permalink
(Start to) handle cli hyperlinks in the terminal used for pkg dev tas…
Browse files Browse the repository at this point in the history
…ks (#5231)

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.

<img width="544" alt="Screenshot 2024-11-18 at 1 04 51 PM"
src="https://github.com/user-attachments/assets/ddead5f3-ee5c-4f36-a50b-12f5ac7e6465">

(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:
r-lib/cli#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
(microsoft/vscode#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.

<img width="544" alt="Screenshot 2024-11-18 at 1 04 51 PM"
src="https://github.com/user-attachments/assets/24a880e0-9ebe-4d88-8051-f859480b04fa">

<img width="285" alt="Screenshot 2024-11-18 at 12 55 37 PM"
src="https://github.com/user-attachments/assets/d1187098-6a76-44b5-b5a5-be8752c43e5f">

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
r-lib/testthat#2025 and unrelated to this PR).

<img width="780" alt="Screenshot 2024-11-18 at 12 55 47 PM"
src="https://github.com/user-attachments/assets/f7dff7ea-1bf4-4e52-8eb5-e92fd3481cda">
  • Loading branch information
jennybc authored Nov 25, 2024
1 parent 81fce8f commit 9084df0
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 9 deletions.
8 changes: 7 additions & 1 deletion extensions/positron-r/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "%displayName%",
"description": "%description%",
"version": "0.0.2",
"publisher": "vscode",
"publisher": "positron",
"engines": {
"vscode": "^1.65.0"
},
Expand Down Expand Up @@ -281,6 +281,12 @@
},
"default": [],
"description": "%r.configuration.extraArguments.description%"
},
"positron.r.taskHyperlinks": {
"scope": "window",
"type": "boolean",
"default": false,
"description": "%r.configuration.taskHyperlinks.description%"
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion extensions/positron-r/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
4 changes: 4 additions & 0 deletions extensions/positron-r/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand All @@ -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 => {
Expand Down
2 changes: 1 addition & 1 deletion extensions/positron-r/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface RPackageInstallation {
compatible: boolean;
}

interface EnvVar {
export interface EnvVar {
[key: string]: string;
}

Expand Down
8 changes: 6 additions & 2 deletions extensions/positron-r/src/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -39,7 +40,7 @@ export async function getRPackageTasks(editorFilePath?: string): Promise<vscode.
message: vscode.l10n.t('{taskName}', { taskName: 'Check R package' }),
rcode: 'devtools::check()',
package: 'devtools',
envVars: null
envVars: { ... await prepCliEnvVars() }
},
{
task: 'r.task.packageInstall',
Expand All @@ -53,7 +54,10 @@ export async function getRPackageTasks(editorFilePath?: string): Promise<vscode.
message: vscode.l10n.t('{taskName}', { taskName: 'Test R package' }),
rcode: 'devtools::test()',
package: 'devtools',
envVars: await getEnvVars(['TESTTHAT_MAX_FAILS'])
envVars: {
... await getEnvVars(['TESTTHAT_MAX_FAILS']),
... await prepCliEnvVars()
}
},
{
task: 'r.task.rmarkdownRender',
Expand Down
99 changes: 99 additions & 0 deletions extensions/positron-r/src/uri-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2024 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';

import { LOGGER } from './extension';
import { RSessionManager } from './session-manager';
import { EnvVar, RSession } from './session';

export async function registerUriHandler() {
vscode.window.registerUriHandler({ handleUri });
}

// Temporary feature flag to finesse the fact that cli hyperlinks are either all ON or all OFF.
// cli 3.6.3.9001 gained support for configuring the URL format of run/help/vignette hyperlinks.
// But file hyperlinks are not yet configurable and will delegate to operating system.
// If the user still has RStudio as the app associated with .R files, it will open in RStudio.
// Flag will be removed once cli can be configured to emit positron://file/... hyperlinks.
function taskHyperlinksEnabled(): boolean {
const extConfig = vscode.workspace.getConfiguration('positron.r');
const taskHyperlinksEnabled = extConfig.get<boolean>('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<EnvVar> {
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 */
};
}
2 changes: 1 addition & 1 deletion extensions/positron-reticulate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
},
"extensionDependencies": [
"ms-python.python",
"vscode.positron-r",
"positron.positron-r",
"vscode.jupyter-adapter"
],
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 9084df0

Please sign in to comment.