-
Notifications
You must be signed in to change notification settings - Fork 92
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2007 from posit-dev/feature/x-r-run
Implement `x-r-run` handler
- Loading branch information
Showing
6 changed files
with
142 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -503,7 +503,7 @@ | |
}, | ||
"positron": { | ||
"binaryDependencies": { | ||
"ark": "0.1.40" | ||
"ark": "0.1.41" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (C) 2024 Posit Software, PBC. All rights reserved. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
import * as positron from 'positron'; | ||
import * as vscode from 'vscode'; | ||
import { randomUUID } from 'crypto'; | ||
import { RRuntime } from './runtime'; | ||
|
||
export async function handleRCode(runtime: RRuntime, code: string): Promise<void> { | ||
const match = matchRunnable(code); | ||
|
||
if (!match) { | ||
// Didn't match our regex. Not safe to run, or not recognizable as R code. | ||
return handleNotRunnable(code); | ||
} | ||
if (!match.groups) { | ||
return handleNotRunnable(code); | ||
} | ||
|
||
const packageName = match.groups.package; | ||
// Not currently used, but could be useful for showing help documentation on hover | ||
// const functionName = match.groups.function; | ||
|
||
if (isCorePackage(packageName)) { | ||
// We never run code prefixed with a core package name, as this is suspicious | ||
return handleNotRunnable(code); | ||
} | ||
|
||
if (isBlessedPackage(packageName)) { | ||
// Attached or not, if it is a blessed package then we automatically run it | ||
return handleAutomaticallyRunnable(runtime, code); | ||
} | ||
if (await runtime.isPackageAttached(packageName)) { | ||
// Only automatically run unknown package code if the package is already attached | ||
return handleAutomaticallyRunnable(runtime, code); | ||
} | ||
|
||
// Otherwise, it looks like runnable code but isn't safe enough to automatically run | ||
return handleManuallyRunnable(runtime, code); | ||
} | ||
|
||
function handleNotRunnable(code: string) { | ||
vscode.window.showInformationMessage(vscode.l10n.t( | ||
`Code hyperlink not recognized. Manually run the following if you trust the hyperlink source: \`${code}\`.` | ||
)); | ||
} | ||
|
||
function handleManuallyRunnable(_runtime: RRuntime, code: string) { | ||
// TODO: Put `code` in the Console for the user, overwriting anything that was there. | ||
// Seems like we will need a new API for that. | ||
vscode.window.showInformationMessage(vscode.l10n.t( | ||
`Code hyperlink written to clipboard: \`${code}\`.` | ||
)); | ||
vscode.env.clipboard.writeText(code); | ||
} | ||
|
||
function handleAutomaticallyRunnable(runtime: RRuntime, code: string) { | ||
const id = randomUUID(); | ||
|
||
// Fire and forget style | ||
runtime.execute( | ||
code, | ||
id, | ||
positron.RuntimeCodeExecutionMode.Interactive, | ||
positron.RuntimeErrorBehavior.Continue | ||
); | ||
} | ||
|
||
export function matchRunnable(code: string): RegExpMatchArray | null { | ||
// Of the form `package::function(args)` where `args` can't contain `(`, `)`, or `;`. | ||
// See https://cli.r-lib.org/reference/links.html#security-considerations. | ||
const runnableRegExp = /^(?<package>\w+)::(?<function>\w+)[(][^();]*[)]$/; | ||
return code.match(runnableRegExp); | ||
} | ||
|
||
function isCorePackage(packageName: string): boolean { | ||
const corePackages = ['utils', 'base', 'stats']; | ||
return corePackages.includes(packageName); | ||
} | ||
|
||
function isBlessedPackage(packageName: string): boolean { | ||
const blessedPackages = ['testthat', 'rlang', 'devtools', 'usethis', 'pkgload', 'pkgdown']; | ||
return blessedPackages.includes(packageName); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (C) 2024 Posit Software, PBC. All rights reserved. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
import * as assert from 'assert'; | ||
import { matchRunnable } from '../hyperlink'; | ||
|
||
suite('Hyperlink', () => { | ||
test('Runnable R code regex enforces safety rules', async () => { | ||
const matchSimple = matchRunnable("pkg::fun()"); | ||
assert.strictEqual(matchSimple?.groups?.package, "pkg"); | ||
assert.strictEqual(matchSimple?.groups?.function, "fun"); | ||
|
||
const matchArgs = matchRunnable("pkg::fun(1 + 1, 2:5)"); | ||
assert.strictEqual(matchArgs?.groups?.package, "pkg"); | ||
assert.strictEqual(matchArgs?.groups?.function, "fun"); | ||
|
||
const matchUnsafeInnerFunction = matchRunnable("pkg::fun(fun())"); | ||
assert.strictEqual(matchUnsafeInnerFunction, null); | ||
|
||
const matchUnsafeSemicolon = matchRunnable("pkg::fun({1 + 2; 3 + 4})"); | ||
assert.strictEqual(matchUnsafeSemicolon, null); | ||
|
||
const matchUnsafeTripleColon = matchRunnable("pkg:::fun()"); | ||
assert.strictEqual(matchUnsafeTripleColon, null); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters