Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 42 additions & 30 deletions extensions/positron-r/src/packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,9 @@ export class RPackageManager {
const pkgVector = this._formatRVector(pkgSpecs);
code = `renv::install(${pkgVector}, lock = TRUE, prompt = FALSE)`;
} else {
// If we're installing pak, don't prompt to install pak
let method: string;
if (packages.some((pkg) => pkg.name === 'pak')) {
method = await this._getPakMethod();
} else {
method = await this._ensurePak();
}
// If we're installing pak itself, don't try to install pak as a side effect.
const installingPak = packages.some((pkg) => pkg.name === 'pak');
const method = await this._resolveMethod(!installingPak);

if (method === 'pak') {
// pak supports "pkg@version" syntax directly
Expand Down Expand Up @@ -125,7 +121,7 @@ export class RPackageManager {
const pkgVector = this._formatRVector(pkgSpecs);
code = `renv::install(${pkgVector}, lock = TRUE, prompt = FALSE)`;
} else {
const method = await this._ensurePak();
const method = await this._resolveMethod(true);

if (method === 'pak') {
// pak supports "pkg@version" syntax directly
Expand Down Expand Up @@ -159,7 +155,7 @@ export class RPackageManager {
if (isRenv) {
await this._execute(`renv::update(lock = TRUE, prompt = FALSE)`, token);
} else {
const method = await this._ensurePak();
const method = await this._resolveMethod(true);

if (method === 'pak') {
// Get outdated packages via RPC, then update with pak
Expand Down Expand Up @@ -203,7 +199,7 @@ export class RPackageManager {
if (isRenv) {
code = `renv::remove(${pkgVector})`;
} else {
const method = await this._getPakMethod();
const method = await this._resolveMethod(false);

if (method === 'pak') {
code = `pak::pkg_remove(${pkgVector})`;
Expand Down Expand Up @@ -242,7 +238,7 @@ export class RPackageManager {
throw new vscode.CancellationError();
}

const method = await this._getPakMethod();
const method = await this._resolveMethod(false);
const result = await this._callMethod<positron.LanguageRuntimePackage[] | null>(
'pkg_search', token, query, method
);
Expand Down Expand Up @@ -294,21 +290,24 @@ export class RPackageManager {
}

/**
* Get the method string based on pak availability (without prompting).
* Read the configured R packages installer preference.
* 'auto' means: prefer pak, prompt to install it when missing, fall back to base on decline.
* 'pak' means: prefer pak, install it without prompting when missing.
* 'base' means: never use or install pak.
*/
private async _getPakMethod(): Promise<string> {
const hasPak = await this._detectPak();
return hasPak ? 'pak' : 'base';
private _getConfiguredInstaller(): 'auto' | 'pak' | 'base' {
const value = vscode.workspace.getConfiguration('packages.r').get<string>('installer');
return value === 'pak' || value === 'base' ? value : 'auto';
}

/**
* Get the method string for package listing (renv > pak > base).
* Get the method string for package listing (renv > resolved method).
*/
private async _getPackageMethod(): Promise<string> {
if (await this._detectRenv()) {
return 'renv';
}
return this._getPakMethod();
return this._resolveMethod(false);
}

/**
Expand All @@ -325,29 +324,42 @@ export class RPackageManager {
}

/**
* Ensure pak is available, prompting to install if needed.
* Returns 'pak' or 'base' depending on availability.
* Resolve which installer to use, honoring the `packages.r.installer` setting.
*
* @param allowInstallPak When true (install/update operations), may install pak, either
* by prompting the user (setting: 'auto') or silently (setting: 'pak').
* When false (list/search/uninstall), only detects what is available.
*/
private async _ensurePak(): Promise<string> {
const hasPak = await this._detectPak();
if (hasPak) {
private async _resolveMethod(allowInstallPak: boolean): Promise<string> {
const setting = this._getConfiguredInstaller();
if (setting === 'base') {
return 'base';
}

if (await this._detectPak()) {
return 'pak';
}

if (this._pakDeclined) {
if (!allowInstallPak) {
return 'base';
}

const install = await this._promptInstallPak();
if (install) {
// Use base R to install pak
if (setting === 'pak') {
// User opted in to pak; install without prompting.
await this._execute('install.packages("pak")');
const nowHasPak = await this._detectPak();
return nowHasPak ? 'pak' : 'base';
} else {
this._pakDeclined = true;
return (await this._detectPak()) ? 'pak' : 'base';
}

// setting === 'auto': prompt once per session, remember declines.
if (this._pakDeclined) {
return 'base';
}
if (await this._promptInstallPak()) {
await this._execute('install.packages("pak")');
return (await this._detectPak()) ? 'pak' : 'base';
}
this._pakDeclined = true;
return 'base';
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,19 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
scope: ConfigurationScope.APPLICATION,
description: nls.localize('positron.packages.enable', 'Show the Packages pane.'),
markdownDeprecationMessage: nls.localize('positron.packages.enable.deprecated', "Deprecated. Use `#packages.enabled#` instead."),
},
'packages.r.installer': {
type: 'string',
enum: ['auto', 'pak', 'base'],
enumDescriptions: [
nls.localize('positron.packages.r.installer.auto', "Use pak if installed; otherwise prompt and fall back to base R."),
nls.localize('positron.packages.r.installer.pak', "Always use pak. Silently install it if missing."),
nls.localize('positron.packages.r.installer.base', "Always use base R."),
],
default: 'auto',
scope: ConfigurationScope.RESOURCE,
markdownDescription: nls.localize('positron.packages.r.installer', "Which package installer to use for installing, updating, and removing R packages. Does not affect projects using renv, which always use renv."),
tags: ['preview'],
}
}
});
Expand Down
Loading