Skip to content

Commit

Permalink
New function to expose more info about an installed package
Browse files Browse the repository at this point in the history
Why?

* Sometimes you want to get info about a package version without necessarily challenging the user to install/upgrade.
* If you do need to nudge the user, this extra info allows us to build a better message.

I also removed the caching, because the extra info makes that increasingly awkward.
  • Loading branch information
jennybc committed Nov 15, 2024
1 parent 39536c2 commit c1e9d6d
Showing 1 changed file with 97 additions and 55 deletions.
152 changes: 97 additions & 55 deletions extensions/positron-r/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import { getPandocPath } from './pandoc';

interface RPackageInstallation {
packageName: string;
packageVersion?: string;
packageVersion: string;
checkAgainst: string;
checkResult: boolean;
}

interface EnvVar {
Expand All @@ -33,6 +35,7 @@ interface EnvVar {
// locale to also be present here, such as LC_CTYPE or LC_TIME. These can vary by OS, so this
// interface doesn't attempt to enumerate them.
interface Locale {
// eslint-disable-next-line @typescript-eslint/naming-convention
LANG: string;
[key: string]: string;
}
Expand Down Expand Up @@ -76,9 +79,6 @@ export class RSession implements positron.LanguageRuntimeSession, vscode.Disposa
/** A timestamp assigned when the session was created. */
private _created: number;

/** Cache for which packages we know are installed in this runtime **/
private _packageCache = new Array<RPackageInstallation>();

/** The current dynamic runtime state */
public dynState: positron.LanguageRuntimeDynState;

Expand Down Expand Up @@ -385,71 +385,113 @@ export class RSession implements positron.LanguageRuntimeSession, vscode.Disposa
}

/**
* Checks whether a package is installed in the runtime.
* @param pkgName The name of the package to check
* @param pkgVersion Optionally, the version of the package needed
* @returns true if the package is installed, false otherwise
* Gets information from the runtime about a specific installed package (or maybe not
* installed).
* @param pkgName The name of the package to check.
* @param checkAgainst Optionally, a minimum version to check for. This may seem weird, but we
* need R to compare versions for us. We can't easily do it over here.
* @returns An instance of RPackageInstallation if the package is installed, `null` otherwise.
*/

async checkInstalled(pkgName: string, pkgVersion?: string): Promise<boolean> {
let isInstalled: boolean;
// Check the cache first
if (this._packageCache.includes({ packageName: pkgName, packageVersion: pkgVersion }) ||
(pkgVersion === undefined && this._packageCache.some(p => p.packageName === pkgName))) {
return true;
}
async packageVersion(pkgName: string, checkAgainst?: string): Promise<RPackageInstallation | null> {
let pkg: any;
try {
if (pkgVersion) {
isInstalled = await this.callMethod('is_installed', pkgName, pkgVersion);
if (checkAgainst) {
pkg = await this.callMethod('packageVersion', pkgName, checkAgainst);
} else {
isInstalled = await this.callMethod('is_installed', pkgName);
pkg = await this.callMethod('packageVersion', pkgName);
}
} catch (err) {
const runtimeError = err as positron.RuntimeMethodError;
throw new Error(`Error checking for package ${pkgName}: ${runtimeError.message} ` +
throw new Error(`Error getting version of package ${pkgName}: ${runtimeError.message} ` +
`(${runtimeError.code})`);
}

if (!isInstalled) {
const message = pkgVersion ? vscode.l10n.t('Package `{0}` version `{1}` required but not installed.', pkgName, pkgVersion)
: vscode.l10n.t('Package `{0}` required but not installed.', pkgName);
const install = await positron.window.showSimpleModalDialogPrompt(
vscode.l10n.t('Missing R package'),
message,
vscode.l10n.t('Install now')
);
if (install) {
const id = randomUUID();

// A promise that resolves when the runtime is idle:
const promise = new Promise<void>(resolve => {
const disp = this.onDidReceiveRuntimeMessage(runtimeMessage => {
if (runtimeMessage.parent_id === id &&
runtimeMessage.type === positron.LanguageRuntimeMessageType.State) {
const runtimeMessageState = runtimeMessage as positron.LanguageRuntimeState;
if (runtimeMessageState.state === positron.RuntimeOnlineState.Idle) {
resolve();
disp.dispose();
}
}
});
});
if (pkg.version === null) {
return null;
}

this.execute(`install.packages("${pkgName}")`,
id,
positron.RuntimeCodeExecutionMode.Interactive,
positron.RuntimeErrorBehavior.Continue);
const pkgInst: RPackageInstallation = {
packageName: pkgName,
packageVersion: pkg.version,
checkAgainst: pkg.checkAgainst,
checkResult: pkg.checkResult
};

// Wait for the the runtime to be idle, or for the timeout:
await Promise.race([promise, timeout(2e4, 'waiting for package installation')]);
return pkgInst;
}

return true;
} else {
return false;
}
/**
* Checks whether a package is installed in the runtime, possibly at a minimum version. If not,
* prompts the user to install the package.
* @param pkgName The name of the package to check.
* @param pkgVersion Optionally, the version of the package needed.
* @returns true if the package is installed, false otherwise
*/

async checkInstalled(pkgName: string, pkgVersion?: string): Promise<boolean> {
let pkgInst: RPackageInstallation | null = null;
if (pkgVersion) {
pkgInst = await this.packageVersion(pkgName, pkgVersion);
} else {
pkgInst = await this.packageVersion(pkgName);
}

const isInstalled = pkgInst !== null;

if (isInstalled && pkgVersion === undefined) {
return true;
}

const isSufficient = pkgInst?.checkResult ?? false;

if (isSufficient) {
return true;
}
this._packageCache.push({ packageName: pkgName, packageVersion: pkgVersion });
// either the package is not installed or its version is insufficient

const title = isInstalled ? vscode.l10n.t('Insufficient package version') : vscode.l10n.t('Missing R package');
const message = isInstalled ?
vscode.l10n.t(
'The {0} package is installed at version {1}, but version {2} is required.',
pkgName, pkgInst!.packageVersion, pkgVersion as string
) : vscode.l10n.t('The {0} package is required, but not installed.', pkgName);
const okButtonTitle = isInstalled ? vscode.l10n.t('Update now') : vscode.l10n.t('Install now');

const install = await positron.window.showSimpleModalDialogPrompt(
title,
message,
okButtonTitle
);
if (!install) {
return false;
}

const id = randomUUID();

// A promise that resolves when the runtime is idle:
const promise = new Promise<void>(resolve => {
const disp = this.onDidReceiveRuntimeMessage(runtimeMessage => {
if (runtimeMessage.parent_id === id &&
runtimeMessage.type === positron.LanguageRuntimeMessageType.State) {
const runtimeMessageState = runtimeMessage as positron.LanguageRuntimeState;
if (runtimeMessageState.state === positron.RuntimeOnlineState.Idle) {
resolve();
disp.dispose();
}
}
});
});

this.execute(`install.packages("${pkgName}")`,
id,
positron.RuntimeCodeExecutionMode.Interactive,
positron.RuntimeErrorBehavior.Continue);

// Wait for the the runtime to be idle, or for the timeout:
await Promise.race([promise, timeout(2e4, 'waiting for package installation')]);

return true;

}

async isPackageAttached(packageName: string): Promise<boolean> {
Expand Down

0 comments on commit c1e9d6d

Please sign in to comment.