Repository: posit-dev/positron
Pull request #6655: add setting to override available R interpreters
Author: login: sharon-wang
Created: 2025-03-05T22:03:44Z | Merged: 2025-03-14T16:11:57Z

### Summary
- addresses https://github.com/posit-dev/positron/issues/6206
- introduces new setting `positron.r.interpreters.override`
    - takes precedence over over the `positron.r.interpreters.exclude`, `positron.r.customBinaries` and `positron.r.customRootFolders` settings

#### Settings UI

<img width="885" alt="image" src="https://github.com/user-attachments/assets/d57335c7-c88a-4b78-a171-12152a7d22a5" />

#### Settings JSON

```json
    "positron.r.interpreters.override": [
        "/opt/local/R/4.3-arm64/Resources/bin/R"
    ]
```

#### Debug Command
`r.interpreters.settingsInfo`

```js
2025-03-14 11:02:50.252 [info] =====================================================================
2025-03-14 11:02:50.252 [info] =============== [START] R INTERPRETER SETTINGS INFO =================
2025-03-14 11:02:50.252 [info] =====================================================================
2025-03-14 11:02:50.252 [info] R interpreter settings: {
  "interpreters.override": [],
  "interpreters.exclude": [],
  "customRootFolders": [
    "/Users/sashimi/scratch"
  ],
  "customBinaries": []
}
2025-03-14 11:02:50.252 [info] =====================================================================
2025-03-14 11:02:50.252 [info] ================ [END] R INTERPRETER SETTINGS INFO ==================
2025-03-14 11:02:50.252 [info] =====================================================================
```

### Release Notes

#### New Features

- The set of available R interpreters can now be overridden via settings (#6206)

#### Bug Fixes

- N/A


### QA Notes

No additional notes!

@:interpreters


Diff:
diff --git a/extensions/positron-r/package.json b/extensions/positron-r/package.json
index 937cb7b98ad0..ef0d87528304 100644
--- a/extensions/positron-r/package.json
+++ b/extensions/positron-r/package.json
@@ -142,6 +142,11 @@
         "category": "R",
         "title": "%r.command.rmarkdownRender.title%",
         "icon": "$(play)"
+      },
+      {
+        "command": "r.interpreters.settingsInfo",
+        "category": "R",
+        "title": "%r.command.interpreters.settingsInfo.title%"
       }
     ],
     "configuration": [
@@ -217,6 +222,15 @@
             "default": [],
             "markdownDescription": "%r.configuration.interpreters.exclude.markdownDescription%"
           },
+          "positron.r.interpreters.override": {
+            "scope": "window",
+            "type": "array",
+            "items": {
+              "type": "string"
+            },
+            "default": [],
+            "markdownDescription": "%r.configuration.interpreters.override.markdownDescription%"
+          },
           "positron.r.interpreters.default": {
             "scope": "window",
             "type": "string",
diff --git a/extensions/positron-r/package.nls.json b/extensions/positron-r/package.nls.json
index e3b61ca9a76b..b4cfca303702 100644
--- a/extensions/positron-r/package.nls.json
+++ b/extensions/positron-r/package.nls.json
@@ -20,6 +20,7 @@
 	"r.command.packageDocument.title": "Document R Package",
 	"r.command.selectInterpreter.title": "Select Interpreter",
 	"r.command.rmarkdownRender.title": "Render Document With R Markdown",
+	"r.command.interpreters.settingsInfo.title": "Print interpreter settings information to Output",
 	"r.menu.createNewFile.title": "R File",
 	"r.menu.insertPipe.title": "Insert the pipe operator",
 	"r.menu.insertLeftAssignment.title": "Insert the \"<-\" assignment operator",
@@ -36,6 +37,7 @@
 	"r.configuration.customRootFolders.markdownDescription": "List of additional folders to search for R installations. These folders are searched after and in the same way as the default folder for your operating system (e.g. `C:/Program Files/R` on Windows).",
 	"r.configuration.customBinaries.markdownDescription": "List of additional R binaries. If you want to use an R installation that is not automatically discovered, provide the path to its binary here. For example, on Windows this might look like `C:/some/unusual/location/R-4.4.1/bin/x64/R.exe`.",
 	"r.configuration.interpreters.exclude.markdownDescription": "List of absolute paths to R binaries or folders containing R binaries to exclude from the available R installations. These interpreters will not be displayed in the Positron UI.\n\nExample: On Linux, add `/custom/location/R/4.3.0/bin/R` to exclude the specific binary, or `/custom/location` to exclude any R installations within the directory.\n\nRequires a restart to take effect.",
+	"r.configuration.interpreters.override.markdownDescription": "List of absolute paths to R binaries or folders containing R binaries to override the list of the available R installations. Only the interpreters found at the specified paths will be displayed in the Positron UI.\n\nThis setting takes precedence over the `#positron.r.interpreters.exclude#`, `#positron.r.customBinaries#` and `#positron.r.customRootFolders#` settings.\n\nExample: On Linux or Mac, add `/custom/location/R/4.3.0/bin/R` to include only this specific installation, or `/custom/location` to include only R installations found within the directory.\n\nRequires a restart to take effect.",
 	"r.configuration.interpreters.default.markdownDescription": "Absolute path to the default R binary to use for new workspaces. This setting no longer applies once you select an R interpreter for the workspace.\n\nExample: On Linux, add `/custom/location/R/4.3.0/bin/R` to set the default interpreter to the specific R binary.\n\nRequires a restart to take effect.",
 	"r.configuration.kernelPath.description": "Path on disk to the ARK kernel executable; use this to override the default (embedded) kernel. Note that this is not the path to R.",
 	"r.configuration.tracing.description": "Traces the communication between VS Code and the language server",
diff --git a/extensions/positron-r/src/commands.ts b/extensions/positron-r/src/commands.ts
index af64934736fd..557be574562a 100644
--- a/extensions/positron-r/src/commands.ts
+++ b/extensions/positron-r/src/commands.ts
@@ -16,6 +16,8 @@ import { MINIMUM_RENV_VERSION, MINIMUM_R_VERSION } from './constants';
 import { RRuntimeManager } from './runtime-manager';
 import { RMetadataExtra } from './r-installation';
 import { onDidDiscoverTestFiles } from './testing/testing';
+import { LOGGER } from './extension.js';
+import { printInterpreterSettingsInfo } from './interpreter-settings.js';
 
 export async function registerCommands(context: vscode.ExtensionContext, runtimeManager: RRuntimeManager) {
 
@@ -224,6 +226,13 @@ export async function registerCommands(context: vscode.ExtensionContext, runtime
 				console.debug('[r.renvInit] renv is not installed');
 			}
 		}),
+
+		// Command used to open up the R Language Pack output channel and log information
+		// about the R interpreter settings
+		vscode.commands.registerCommand('r.interpreters.settingsInfo', async () => {
+			LOGGER.show();
+			printInterpreterSettingsInfo();
+		}),
 	);
 }
 
diff --git a/extensions/positron-r/src/interpreter-settings.ts b/extensions/positron-r/src/interpreter-settings.ts
new file mode 100644
index 000000000000..a3d306687aa5
--- /dev/null
+++ b/extensions/positron-r/src/interpreter-settings.ts
@@ -0,0 +1,184 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (C) 2025 Posit Software, PBC. All rights reserved.
+ *  Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as path from 'path';
+import * as vscode from 'vscode';
+import { LOGGER } from './extension';
+import { arePathsSame, isParentPath, untildify } from './path-utils';
+
+/**
+ * Directory(ies) where this user keeps R installations.
+ * Converts aliased paths to absolute paths. Relative paths are ignored.
+ * @returns List of directories to scan for R installations.
+ */
+export function userRHeadquarters(): string[] {
+	const config = vscode.workspace.getConfiguration('positron.r');
+	const customRootFolders = config.get<string[]>('customRootFolders') ?? [];
+	if (customRootFolders.length === 0) {
+		LOGGER.debug('No custom root folders specified via positron.r.customRootFolders');
+		return [];
+	}
+	const userHqDirs = customRootFolders
+		.map((item) => untildify(item))
+		.filter((item) => {
+			if (path.isAbsolute(item)) {
+				return true;
+			}
+			LOGGER.info(`R custom root folder path ${item} is not absolute...ignoring`);
+			return false;
+		});
+	const formattedPaths = JSON.stringify(userHqDirs, null, 2);
+	LOGGER.info(`Directories from 'positron.r.customRootFolders' to scan for R installations:\n${formattedPaths}`);
+	return userHqDirs;
+}
+
+/**
+ * Ad hoc R binaries the user wants Positron to know about.
+ * @returns List of custom R binaries specified by the user.
+ */
+export function userRBinaries(): string[] {
+	const config = vscode.workspace.getConfiguration('positron.r');
+	const customBinaries = config.get<string[]>('customBinaries') ?? [];
+	if (customBinaries.length === 0) {
+		LOGGER.debug('No custom binaries specified via positron.r.customBinaries');
+		return [];
+	}
+	const userBinaries = customBinaries
+		.map((item) => untildify(item))
+		.filter((item) => {
+			if (path.isAbsolute(item)) {
+				return true;
+			}
+			LOGGER.info(`R custom binary path ${item} is not absolute...ignoring`);
+			return false;
+		});
+	const formattedPaths = JSON.stringify(userBinaries, null, 2);
+	LOGGER.info(`R binaries from 'positron.r.customBinaries' to discover:\n${formattedPaths}`);
+	return userBinaries;
+}
+
+/**
+ * Gets the list of R installations excluded via settings.
+ * Converts aliased paths to absolute paths. Relative paths are ignored.
+ * @returns List of installation paths to exclude.
+ */
+function getExcludedInstallations(): string[] {
+	const config = vscode.workspace.getConfiguration('positron.r');
+	const interpretersExclude = config.get<string[]>('interpreters.exclude') ?? [];
+	if (interpretersExclude.length === 0) {
+		LOGGER.debug('No installation paths specified to exclude via positron.r.interpreters.exclude');
+		return [];
+	}
+	const excludedPaths = interpretersExclude
+		.map((item) => untildify(item))
+		.filter((item) => {
+			if (path.isAbsolute(item)) {
+				return true;
+			}
+			LOGGER.info(`R installation path to exclude ${item} is not absolute...ignoring`);
+			return false;
+		});
+	const formattedPaths = JSON.stringify(excludedPaths, null, 2);
+	LOGGER.info(`R installation paths from 'positron.r.interpreters.exclude' to exclude:\n${formattedPaths}`);
+	return excludedPaths;
+}
+
+/**
+ * Gets the list of R installations to override the installations we make available.
+ * The override setting take precedence over the excluded installations, the custom binaries
+ * and the custom root folders settings.
+ * Converts aliased paths to absolute paths. Relative paths are ignored.
+ * @returns List of installation paths to exclusively include.
+ */
+export function getInterpreterOverridePaths(): string[] {
+	const config = vscode.workspace.getConfiguration('positron.r');
+	const interpretersOverride = config.get<string[]>('interpreters.override') ?? [];
+	if (interpretersOverride.length === 0) {
+		LOGGER.debug('No installation paths specified to exclusively include via positron.r.interpreters.override');
+		return [];
+	}
+	const overridePaths = interpretersOverride
+		.map((item) => untildify(item))
+		.filter((item) => {
+			if (path.isAbsolute(item)) {
+				return true;
+			}
+			LOGGER.info(`R installation path to exclusively include ${item} is not absolute...ignoring`);
+			return false;
+		});
+	const formattedPaths = JSON.stringify(overridePaths, null, 2);
+	LOGGER.info(`R installation paths from 'positron.r.interpreters.override' to exclusively include:\n${formattedPaths}`);
+	return overridePaths;
+}
+
+/**
+ * Checks if the given binary path is excluded via settings. If interpreter override paths are
+ * specified, this method will return true if the binary path is not in the override paths. The
+ * override paths take precedence over the excluded installations.
+ * @param binpath The binary path to check
+ * @returns True if the binary path is excluded, false if it is not excluded, and undefined if the
+ * no exclusions have been specified.
+ */
+export function isExcludedInstallation(binpath: string): boolean | undefined {
+	const overridePaths = getInterpreterOverridePaths();
+	if (overridePaths.length > 0) {
+		// Override paths are exclusive include paths, so an interpreter is excluded if it is not in
+		// the override paths.
+		return !overridePaths.some(
+			override => isParentPath(binpath, override) || arePathsSame(binpath, override)
+		);
+	}
+
+	const excludedInstallations = getExcludedInstallations();
+	if (excludedInstallations.length === 0) {
+		return undefined;
+	}
+	return excludedInstallations.some(
+		excluded => isParentPath(binpath, excluded) || arePathsSame(binpath, excluded)
+	);
+}
+
+/**
+ * Get the default R interpreter path specified in Positron settings.
+ * Converts aliased paths to absolute paths. Relative paths are ignored.
+ * @returns The default R interpreter path specified in the settings, or undefined if not set.
+ */
+export function getDefaultInterpreterPath(): string | undefined {
+	const config = vscode.workspace.getConfiguration('positron.r');
+	let defaultInterpreterPath = config.get<string>('interpreters.default');
+	if (defaultInterpreterPath) {
+		defaultInterpreterPath = untildify(defaultInterpreterPath);
+		if (path.isAbsolute(defaultInterpreterPath)) {
+			LOGGER.info(`Default R interpreter path specified in 'positron.r.interpreters.default': ${defaultInterpreterPath}`);
+			return defaultInterpreterPath;
+		}
+		LOGGER.info(`Default R interpreter path ${defaultInterpreterPath} is not absolute...ignoring`);
+		return undefined;
+	}
+	return undefined;
+}
+
+/**
+ * Print the R interpreter settings info to the log.
+ */
+export function printInterpreterSettingsInfo(): void {
+	const interpreterSettingsInfo = {
+		// eslint-disable-next-line @typescript-eslint/naming-convention
+		'interpreters.default': getDefaultInterpreterPath(),
+		// eslint-disable-next-line @typescript-eslint/naming-convention
+		'interpreters.override': getInterpreterOverridePaths(),
+		// eslint-disable-next-line @typescript-eslint/naming-convention
+		'interpreters.exclude': getExcludedInstallations(),
+		'customRootFolders': userRHeadquarters(),
+		'customBinaries': userRBinaries(),
+	};
+	LOGGER.info('=====================================================================');
+	LOGGER.info('=============== [START] R INTERPRETER SETTINGS INFO =================');
+	LOGGER.info('=====================================================================');
+	LOGGER.info('R interpreter settings:', JSON.stringify(interpreterSettingsInfo, null, 2));
+	LOGGER.info('=====================================================================');
+	LOGGER.info('================ [END] R INTERPRETER SETTINGS INFO ==================');
+	LOGGER.info('=====================================================================');
+}
diff --git a/extensions/positron-r/src/path-utils.ts b/extensions/positron-r/src/path-utils.ts
index d3d5658103e2..1ec71940ef6f 100644
--- a/extensions/positron-r/src/path-utils.ts
+++ b/extensions/positron-r/src/path-utils.ts
@@ -5,6 +5,7 @@
 
 import * as path from 'path';
 import * as os from 'os';
+import * as fs from 'fs';
 
 /**
  * Returns true if given file path exists within the given parent directory, false otherwise.
@@ -42,3 +43,48 @@ export function arePathsSame(path1: string, path2: string): boolean {
 export function untildify(path: string): string {
 	return path.replace(/^~($|\/|\\)/, `${os.homedir()}$1`);
 }
+
+/**
+ * Check if a path is a file.
+ * A combination of isFile and resolveSymbolicLink in extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts.
+ * @param filePath The path to check
+ * @returns Whether the path is a file
+ */
+export function isFile(filePath: string): boolean {
+	try {
+		const stats = fs.lstatSync(filePath);
+		if (stats.isSymbolicLink()) {
+			let resolvedPath = fs.realpathSync(filePath);
+			// Stop at a maximum depth of 5 symbolic links to avoid infinite loops
+			const maximumDepth = 5;
+			for (let i = 0; i < maximumDepth; i++) {
+				const resolvedStats = fs.lstatSync(resolvedPath);
+				if (resolvedStats.isFile()) {
+					return true;
+				}
+				resolvedPath = fs.realpathSync(resolvedPath);
+			}
+			console.error(`[isFile] Detected a potential symbolic link loop at ${filePath}, terminating resolution.`);
+			return false;
+		}
+		return stats.isFile();
+	} catch (error) {
+		console.error(`[isFile] Error checking if path is a file: ${error}`);
+		return false;
+	}
+}
+
+/**
+ * Check if a path is a directory.
+ * @param filePath The path to check
+ * @returns Whether the path is a directory
+ */
+export function isDirectory(filePath: string): boolean {
+	try {
+		const stats = fs.lstatSync(filePath);
+		return stats.isDirectory();
+	} catch (error) {
+		console.error(`[isDirectory] Error checking if path is a directory: ${error}`);
+		return false;
+	}
+}
diff --git a/extensions/positron-r/src/provider.ts b/extensions/positron-r/src/provider.ts
index 647f0ed9e69a..ace498cc45ad 100644
--- a/extensions/positron-r/src/provider.ts
+++ b/extensions/positron-r/src/provider.ts
@@ -12,10 +12,11 @@ import * as which from 'which';
 import * as positron from 'positron';
 import * as crypto from 'crypto';
 
-import { RInstallation, RMetadataExtra, getRHomePath, ReasonDiscovered, friendlyReason, getDefaultInterpreterPath } from './r-installation';
+import { RInstallation, RMetadataExtra, getRHomePath, ReasonDiscovered, friendlyReason } from './r-installation';
 import { LOGGER } from './extension';
 import { EXTENSION_ROOT_DIR, MINIMUM_R_VERSION } from './constants';
-import { arePathsSame, untildify } from './path-utils';
+import { getInterpreterOverridePaths, printInterpreterSettingsInfo, userRBinaries, userRHeadquarters } from './interpreter-settings.js';
+import { isDirectory, isFile } from './path-utils.js';
 
 // We don't give this a type so it's compatible with both the VS Code
 // and the LSP types
@@ -28,7 +29,12 @@ export const R_DOCUMENT_SELECTORS = [
 
 export interface RBinary {
 	path: string;
-	reasons: ReasonDiscovered[]
+	reasons: ReasonDiscovered[];
+}
+
+interface DiscoveredBinaries {
+	binaries: RBinary[];
+	currentBinary?: string;
 }
 
 /**
@@ -46,69 +52,29 @@ export enum RRuntimeSource {
  * @param context The extension context.
  */
 export async function* rRuntimeDiscoverer(): AsyncGenerator<positron.LanguageRuntimeMetadata> {
-	const binaries = new Map<string, RBinary>();
-
-	function updateBinaries(binary: RBinary | RBinary[] | undefined): void {
-		if (!binary) {
-			return;
+	// Discover R binaries on the system
+	const { binaries, currentBinary } = await getBinaries();
+
+	// If no R binaries are found, notify the user and end discovery.
+	if (binaries.length === 0) {
+		LOGGER.warn('Positron could not find any R installations. Please review any custom settings.');
+		printInterpreterSettingsInfo();
+		const showLog = await positron.window.showSimpleModalDialogPrompt(
+			vscode.l10n.t('No R installations discovered'),
+			vscode.l10n.t('Positron could not find any R installations. Learn more about R discovery at <br><a href="https://positron.posit.co/r-installations">https://positron.posit.co/r-installations</a>'),
+			vscode.l10n.t('View logs'),
+			vscode.l10n.t('Dismiss')
+		);
+		if (showLog) {
+			LOGGER.show();
 		}
-		const input = Array.isArray(binary) ? binary : [binary];
-		for (const binary of input) {
-			if (binaries.has(binary.path)) {
-				const reasons = new Set(binaries.get(binary.path)!.reasons.concat(binary.reasons));
-				binaries.get(binary.path)!.reasons = Array.from(reasons);
-			} else {
-				binaries.set(binary.path, binary);
-			}
-		}
-	}
-
-	// Find the current R binary(ies) for various definitions of "current".
-	// The first item here will eventually be marked as The Current R Binary.
-	const currentBinaries = await currentRBinaryCandidates();
-	updateBinaries(currentBinaries);
-
-	// Now we scour the system for all R binaries we can find.
-	const systemHqBinaries = discoverHQBinaries(rHeadquarters());
-	updateBinaries(systemHqBinaries);
-
-	const registryBinaries = await discoverRegistryBinaries();
-	updateBinaries(registryBinaries);
-
-	const moreBinaries = discoverAdHocBinaries([
-		'/usr/bin/R',
-		'/usr/local/bin/R',
-		'/opt/local/bin/R',
-		'/opt/homebrew/bin/R'
-	]);
-	updateBinaries(moreBinaries);
-
-	// Optional, user-specified root directories or binaries
-	const userBinaries = discoverUserSpecifiedBinaries();
-	updateBinaries(userBinaries);
-
-	// Search locations for R binaries that are conventional on servers. These locations are also
-	// searched by RStudio/Posit Workbench on POSIX platforms, such as Linux and macOS.
-	// See https://github.com/rstudio/rstudio/blob/bb8cbf17bb415467f87d6e415f9e3777fa46e583/src/cpp/core/r_util/RVersionsPosix.cpp#L121-L147
-	if (os.platform() !== 'win32') {
-		const serverBinaries = discoverServerBinaries();
-		updateBinaries(serverBinaries);
+		return;
 	}
 
-	// (Try to) promote each RBinary to a proper RInstallation
-	let rInstallations: Array<RInstallation> = [];
-	if (currentBinaries.length > 0) {
-		const currentBinary = currentBinaries[0];
-		rInstallations.push(new RInstallation(currentBinary.path, true, currentBinary.reasons));
-		binaries.delete(currentBinary.path);
-	}
-
-	binaries.forEach(rbin => {
-		rInstallations.push(new RInstallation(rbin.path, false, rbin.reasons));
-	});
-
-	const rejectedRInstallations: Array<RInstallation> = [];
-	rInstallations = rInstallations
+	// Promote R binaries to R installations, filtering out any rejected R installations
+	const rejectedRInstallations: RInstallation[] = [];
+	const rInstallations: RInstallation[] = binaries
+		.map(rbin => new RInstallation(rbin.path, rbin.path === currentBinary, rbin.reasons))
 		.filter(r => {
 			if (!r.usable) {
 				LOGGER.info(`Filtering out ${r.binpath}, reason: ${friendlyReason(r.reasonRejected)}.`);
@@ -118,6 +84,7 @@ export async function* rRuntimeDiscoverer(): AsyncGenerator<positron.LanguageRun
 			return true;
 		});
 
+	// Log info about rejected R installations or lack of usable R installations
 	if (rejectedRInstallations.length > 0) {
 		if (rInstallations.length === 0) {
 			LOGGER.warn(`All discovered R installations are unusable by Positron.`);
@@ -137,6 +104,7 @@ export async function* rRuntimeDiscoverer(): AsyncGenerator<positron.LanguageRun
 		}
 	}
 
+	// Sort the R installations
 	rInstallations.sort((a, b) => {
 		if (a.current || b.current) {
 			// always put the current R version first
@@ -154,6 +122,7 @@ export async function* rRuntimeDiscoverer(): AsyncGenerator<positron.LanguageRun
 	// checking an renv lockfile for a match against a system version of R.
 	let recommendedForWorkspace = await shouldRecommendForWorkspace();
 
+	// Construct and yield the metadata for each R installation
 	for (const rInst of rInstallations) {
 		// If we're recommending an R runtime, request immediate startup.
 		const startupBehavior = recommendedForWorkspace ?
@@ -181,6 +150,71 @@ export async function* rRuntimeDiscoverer(): AsyncGenerator<positron.LanguageRun
 	}
 }
 
+/**
+ * Discover binaries on the system based on various sources and return them.
+ * @returns A list of unique R binaries and the current binary if it exists.
+ */
+async function getBinaries(): Promise<DiscoveredBinaries> {
+	// If the override paths are specified, use them exclusively
+	const overrideBinaries = discoverOverrideBinaries();
+	if (overrideBinaries !== undefined) {
+		const uniqueBinaries = deduplicateRBinaries(overrideBinaries);
+		return { binaries: uniqueBinaries, currentBinary: undefined };
+	}
+
+	// Consult various sources of R binaries
+	const currentBinaries = await currentRBinaryCandidates();
+	const systemBinaries = discoverSystemBinaries();
+	const registryBinaries = await discoverRegistryBinaries();
+	const moreBinaries = discoverAdHocBinaries([
+		'/usr/bin/R',
+		'/usr/local/bin/R',
+		'/opt/local/bin/R',
+		'/opt/homebrew/bin/R'
+	]);
+	const userBinaries = discoverUserSpecifiedBinaries();
+	const serverBinaries = discoverServerBinaries();
+
+	// Combine all the binaries we've found
+	const rBinaries: RBinary[] = [
+		...currentBinaries,
+		...systemBinaries,
+		...registryBinaries,
+		...moreBinaries,
+		...userBinaries,
+		...serverBinaries
+	];
+
+	// Deduplicate the binaries
+	const uniqueBinaries = deduplicateRBinaries(rBinaries);
+
+	// Return the array of unique binaries and the current binary if it exists
+	return {
+		binaries: uniqueBinaries,
+		currentBinary: currentBinaries.length > 0 ? currentBinaries[0].path : undefined
+	};
+}
+
+/**
+ * Deduplicate a list of R binaries, merging the reasons for each binary.
+ * @param binaries Binaries to deduplicate
+ * @returns Deduplicated binaries
+ */
+function deduplicateRBinaries(binaries: RBinary[]) {
+	const binariesMap = binaries.reduce((acc, binary) => {
+		if (acc.has(binary.path)) {
+			const existingBinary = acc.get(binary.path)!;
+			const mergedReasons = Array.from(new Set([...existingBinary.reasons, ...binary.reasons]));
+			acc.set(binary.path, { ...existingBinary, reasons: mergedReasons });
+		} else {
+			acc.set(binary.path, binary);
+		}
+		return acc;
+	}, new Map<string, RBinary>());
+
+	return Array.from(binariesMap.values());
+}
+
 export async function makeMetadata(
 	rInst: RInstallation,
 	startupBehavior: positron.LanguageRuntimeStartupBehavior = positron.LanguageRuntimeStartupBehavior.Implicit,
@@ -285,6 +319,11 @@ export async function currentRBinary(): Promise<RBinary | undefined> {
 	}
 }
 
+/**
+ * Get the current R binary(ies) for various definitions of "current".
+ * The first item of the returned list will eventually be marked as The Current R Binary.
+ * @returns List of current R binaries from various sources.
+ */
 async function currentRBinaryCandidates(): Promise<RBinary[]> {
 	const candidates: RBinary[] = [];
 	let candidate: RBinary | undefined;
@@ -551,6 +590,14 @@ function discoverAdHocBinaries(paths: string[]): RBinary[] {
 		.map(b => ({ path: b, reasons: [ReasonDiscovered.adHoc] }));
 }
 
+/**
+ * Scour the system for all R binaries we can find.
+ * @returns System R binaries.
+ */
+function discoverSystemBinaries(): RBinary[] {
+	return discoverHQBinaries(rHeadquarters());
+}
+
 /**
  * Discovers optional, user-specified root directories or binaries.
  * @returns R binaries that the user has specified.
@@ -559,7 +606,7 @@ function discoverUserSpecifiedBinaries(): RBinary[] {
 	const userHqBinaries = discoverHQBinaries(userRHeadquarters());
 	const userMoreBinaries = discoverAdHocBinaries(userRBinaries());
 	const userBinaries = userHqBinaries.concat(userMoreBinaries);
-	// Return the binaries, overwriting the ReasonDiscovered with ReasonDiscovered.user
+	// Return the binaries, overwriting the ReasonDiscovered with ReasonDiscovered.userSetting
 	return userBinaries.map(b => ({ path: b.path, reasons: [ReasonDiscovered.userSetting] }));
 }
 
@@ -567,9 +614,15 @@ function discoverUserSpecifiedBinaries(): RBinary[] {
  * Discovers R binaries that are installed in conventional locations on servers, such as Posit
  * Workbench.
  * Paths are from: https://docs.posit.co/ide/server-pro/r/using_multiple_versions_of_r.html
+ * These locations are also searched by RStudio/Posit Workbench on POSIX platforms, such as Linux and macOS.
+ * See https://github.com/rstudio/rstudio/blob/bb8cbf17bb415467f87d6e415f9e3777fa46e583/src/cpp/core/r_util/RVersionsPosix.cpp#L121-L147
  * @returns R binaries that are installed in conventional locations on servers.
  */
 function discoverServerBinaries(): RBinary[] {
+	if (os.platform() === 'win32') {
+		return [];
+	}
+
 	const serverBinaries = discoverHQBinaries([
 		'/usr/lib/R',
 		'/usr/lib64/R',
@@ -580,10 +633,34 @@ function discoverServerBinaries(): RBinary[] {
 		// '/opt/R', // Already checked for in rHeadquarters
 		'/opt/local/R'
 	]);
+
 	// Return the binaries, overwriting the ReasonDiscovered with ReasonDiscovered.server
 	return serverBinaries.map(b => ({ path: b.path, reasons: [ReasonDiscovered.server] }));
 }
 
+/**
+ * Discovers R binaries that are specified via the `positron.r.interpreters.override` setting.
+ * @returns R binaries that are installed in the settings-specified locations.
+ */
+function discoverOverrideBinaries(): RBinary[] | undefined {
+	const overridePaths = getInterpreterOverridePaths();
+	if (overridePaths.length === 0) {
+		return undefined;
+	}
+
+	// Filter the override paths into directories and files
+	const overrideDirs = overridePaths.filter((item) => isDirectory(item));
+	const overrideFiles = overridePaths.filter((item) => isFile(item));
+
+	// Discover the binaries in the override directories and files and combine them
+	const overrideHqBinaries = discoverHQBinaries(overrideDirs);
+	const overrideAdHocBinaries = discoverAdHocBinaries(overrideFiles);
+	const overrideBinaries = overrideHqBinaries.concat(overrideAdHocBinaries);
+
+	// Return the binaries, overwriting the ReasonDiscovered with ReasonDiscovered.userSetting
+	return overrideBinaries.map(b => ({ path: b.path, reasons: [ReasonDiscovered.userSetting] }));
+}
+
 // R discovery helpers
 
 // directory(ies) where this OS is known to keep its R installations
@@ -607,50 +684,6 @@ function rHeadquarters(): string[] {
 	}
 }
 
-// directory(ies) where this user keeps R installations
-function userRHeadquarters(): string[] {
-	const config = vscode.workspace.getConfiguration('positron.r');
-	const customRootFolders = config.get<string[]>('customRootFolders') ?? [];
-	if (customRootFolders.length > 0) {
-		const userHqDirs = customRootFolders
-			.map((item) => untildify(item))
-			.filter((item) => {
-				if (path.isAbsolute(item)) {
-					return true;
-				}
-				LOGGER.info(`R custom root folder path ${item} is not absolute...ignoring`);
-				return false;
-			});
-		const formattedPaths = JSON.stringify(userHqDirs, null, 2);
-		LOGGER.info(`User-specified directories to scan for R installations:\n${formattedPaths}`);
-		return userHqDirs;
-	} else {
-		return [];
-	}
-}
-
-// ad hoc binaries this user wants Positron to know about
-function userRBinaries(): string[] {
-	const config = vscode.workspace.getConfiguration('positron.r');
-	const customBinaries = config.get<string[]>('customBinaries') ?? [];
-	if (customBinaries.length > 0) {
-		const userBinaries = customBinaries
-			.map((item) => untildify(item))
-			.filter((item) => {
-				if (path.isAbsolute(item)) {
-					return true;
-				}
-				LOGGER.info(`R custom binary path ${item} is not absolute...ignoring`);
-				return false;
-			});
-		const formattedPaths = JSON.stringify(userBinaries, null, 2);
-		LOGGER.info(`User-specified R binaries:\n${formattedPaths}`);
-		return userBinaries;
-	} else {
-		return [];
-	}
-}
-
 function firstExisting(base: string, fragments: string[]): string {
 	const potentialPaths = fragments.map(f => path.join(base, f));
 	const existingPath = potentialPaths.find(p => fs.existsSync(p));
diff --git a/extensions/positron-r/src/r-installation.ts b/extensions/positron-r/src/r-installation.ts
index e9c27bfe2e33..85a7fdb58fdc 100644
--- a/extensions/positron-r/src/r-installation.ts
+++ b/extensions/positron-r/src/r-installation.ts
@@ -6,11 +6,11 @@
 import * as semver from 'semver';
 import * as path from 'path';
 import * as fs from 'fs';
-import * as vscode from 'vscode';
 import { extractValue, readLines, removeSurroundingQuotes } from './util';
 import { LOGGER } from './extension';
 import { MINIMUM_R_VERSION } from './constants';
-import { arePathsSame, isParentPath, untildify } from './path-utils';
+import { arePathsSame } from './path-utils';
+import { getDefaultInterpreterPath, isExcludedInstallation } from './interpreter-settings.js';
 
 /**
  * Extra metadata included in the LanguageRuntimeMetadata for R installations.
@@ -308,64 +308,3 @@ function getRHomePathWindows(binpath: string): string | undefined {
 	}
 
 }
-
-/**
- * Gets the list of R installations excluded via settings.
- * Converts aliased paths to absolute paths. Relative paths are ignored.
- * @returns List of installation paths to exclude.
- */
-function getExcludedInstallations(): string[] {
-	const config = vscode.workspace.getConfiguration('positron.r');
-	const interpretersExclude = config.get<string[]>('interpreters.exclude') ?? [];
-	if (interpretersExclude.length > 0) {
-		const excludedPaths = interpretersExclude
-			.map((item) => untildify(item))
-			.filter((item) => {
-				if (path.isAbsolute(item)) {
-					return true;
-				}
-				LOGGER.info(`R installation path to exclude ${item} is not absolute...ignoring`);
-				return false;
-			});
-		const formattedPaths = JSON.stringify(excludedPaths, null, 2);
-		LOGGER.info(`R installation paths to exclude:\n${formattedPaths}`);
-		return excludedPaths;
-	}
-	LOGGER.debug('No installation paths specified to exclude via settings');
-	return [];
-}
-
-/**
- * Checks if the given binary path is excluded via settings.
- * @param binpath The binary path to check
- * @returns True if the binary path is excluded, false if it is not excluded, and undefined if the
- * no exclusions have been specified.
- */
-function isExcludedInstallation(binpath: string): boolean | undefined {
-	const excludedInstallations = getExcludedInstallations();
-	if (excludedInstallations.length === 0) {
-		return undefined;
-	}
-	return excludedInstallations.some(
-		excluded => isParentPath(binpath, excluded) || arePathsSame(binpath, excluded)
-	);
-}
-
-/**
- * Get the default R interpreter path specified in Positron settings.
- * Converts aliased paths to absolute paths. Relative paths are ignored.
- * @returns The default R interpreter path specified in the settings, or undefined if not set.
- */
-export function getDefaultInterpreterPath(): string | undefined {
-	const config = vscode.workspace.getConfiguration('positron.r');
-	let defaultInterpreterPath = config.get<string>('interpreters.default');
-	if (defaultInterpreterPath) {
-		defaultInterpreterPath = untildify(defaultInterpreterPath);
-		if (path.isAbsolute(defaultInterpreterPath)) {
-			return defaultInterpreterPath;
-		}
-		LOGGER.info(`Default R interpreter path ${defaultInterpreterPath} is not absolute...ignoring`);
-		return undefined;
-	}
-	return undefined;
-}
diff --git a/extensions/positron-r/src/runtime-manager.ts b/extensions/positron-r/src/runtime-manager.ts
index ecfa8b6c7387..95a29ea3a896 100644
--- a/extensions/positron-r/src/runtime-manager.ts
+++ b/extensions/positron-r/src/runtime-manager.ts
@@ -7,12 +7,12 @@ import * as positron from 'positron';
 import * as vscode from 'vscode';
 import * as fs from 'fs';
 import { currentRBinary, makeMetadata, rRuntimeDiscoverer } from './provider';
-import { RInstallation, RMetadataExtra, ReasonDiscovered, friendlyReason, getDefaultInterpreterPath } from './r-installation';
+import { RInstallation, RMetadataExtra, ReasonDiscovered, friendlyReason } from './r-installation';
 import { RSession, createJupyterKernelExtra } from './session';
 import { createJupyterKernelSpec } from './kernel-spec';
-import { untildify } from './path-utils';
 import { LOGGER } from './extension';
 import { POSITRON_R_INTERPRETERS_DEFAULT_SETTING_KEY } from './constants';
+import { getDefaultInterpreterPath } from './interpreter-settings.js';
 
 export class RRuntimeManager implements positron.LanguageRuntimeManager {
 


Reviews:
user: login: sharon-wang
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T19:56:24Z
user: login: jennybc
body: I'd describe this as consulting the overrides inside of some of our existing sub-strategies for R discovery. Which sort of sprinkles this logic around / pushes it down into helpers.

Did you consider solving from the other direction? Meaning: If overrides are specified, just load up those binaries and bypass all the other sub-strategies. As it stands, it's not obvious to me that we're skipping over, e.g., the registry search and ad hoc binaries. (I realize the runtime discoverer is not really organized favorably for this approach right now, but perhaps we should at least discuss a future refactoring that makes things more obvious.)
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T21:33:13Z
user: login: jennybc
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T15:30:11Z
user: login: sharon-wang
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T21:12:45Z
user: login: jennybc
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T21:30:39Z
user: login: jennybc
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T18:09:59Z
user: login: jennybc
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-13T18:20:39Z
user: login: melissa-barca
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T18:40:27Z
user: login: jennybc
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T18:42:53Z
user: login: melissa-barca
body: LGTM!
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T18:49:19Z
user: login: sharon-wang
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T15:09:51Z

Review comments:
diff_hunk: @@ -217,6 +217,15 @@
             "default": [],
             "markdownDescription": "%r.configuration.interpreters.exclude.markdownDescription%"
           },
+          "positron.r.interpreters.override": {
+            "scope": "window",
path: extensions/positron-r/package.json
user: login: sharon-wang
body: the scope will be updated to `machine` with all of the other settings in a separate PR
created_at: 2025-03-11T19:56:24Z
author_association: MEMBER
diff_hunk: @@ -336,12 +336,51 @@ function getExcludedInstallations(): string[] {
 }
 
 /**
- * Checks if the given binary path is excluded via settings.
+ * Gets the list of R installations to override the installations we make available.
+ * The override setting take precedence over the excluded installations, the custom binaries
+ * and the custom root folders settings.
+ * Converts aliased paths to absolute paths. Relative paths are ignored.
+ * @returns List of installation paths to exclusively include.
+ */
+export function getInterpreterOverridePaths(): string[] {
+	const config = vscode.workspace.getConfiguration('positron.r');
+	const interpretersOverride = config.get<string[]>('interpreters.override') ?? [];
+	if (interpretersOverride.length > 0) {
path: extensions/positron-r/src/r-installation.ts
user: login: jennybc
body: Very weakly held opinion: I think I'd check for length zero and get out early. I'm just a big fan of early exits.
created_at: 2025-03-11T21:21:57Z
author_association: MEMBER
diff_hunk: @@ -217,6 +217,15 @@
             "default": [],
             "markdownDescription": "%r.configuration.interpreters.exclude.markdownDescription%"
           },
+          "positron.r.interpreters.override": {
+            "scope": "window",
path: extensions/positron-r/package.json
user: login: jennybc
body: (Apologies in advance if I have confused myself about the intent of this comment. But I'm assuming it's about whether a setting can be configured for a workspace vs. a user.)

Now **this** setting does strike me as relevant to an individual workspace. It feels like someone who isn't concerned about portability or tool agnosticism (e.g. a small team all on workbench?) could use this to pin a specific R version for a single project.
created_at: 2025-03-12T15:30:10Z
author_association: MEMBER
diff_hunk: @@ -217,6 +217,15 @@
             "default": [],
             "markdownDescription": "%r.configuration.interpreters.exclude.markdownDescription%"
           },
+          "positron.r.interpreters.override": {
+            "scope": "window",
path: extensions/positron-r/package.json
user: login: sharon-wang
body: (Yes that's right!)

It feels like these interpreter settings should all be `machine`-scoped as they refer to specific paths on a given machine. But I think I am more opinionated in that the settings should all have the same scope, and less opinionated about the specific scope they should have.

These are the relevant scopes to choose from, for all these interpreter settings I think! From [VS Code docs](https://code.visualstudio.com/api/references/contribution-points#:~:text=machine%20%2D%20Machine%20specific%20settings%20that%20can%20be%20set%20only%20in%20user%20settings%20or%20only%20in%20remote%20settings.%20For%20example%2C%20an%20installation%20path%20which%20shouldn't%20be%20shared%20across%20machines):
> `machine` - Machine specific settings that can be set only in user settings or only in remote settings. For example, an installation path which shouldn't be shared across machines.
>`machine-overridable` - Machine specific settings that can be overridden by workspace or folder settings.
>`window` - Windows (instance) specific settings which can be configured in user, workspace, or remote settings.

To pin a version of R for a single project sounds like #2659. I feel like the `override` setting is mainly for filtering down the list of available interpreters for admin purposes. Although pinning a version for R could be achieved by specifying a specific interpreter in the `override` option, it feels like a slightly different affordance to me. I am finding it to be intertwined with the Affiliated Runtime concept, which currently only exists on a user's machine and isn't configurable via settings. Perhaps there could be a separate, workspace-level way to configure the affiliated runtime?

Maybe @melissa-barca can comment a bit more on how Workbench Admins may apply user settings for subgroups of users? Is there appetite for any/all of interpreter includes/excludes/overrides/default interpreter path being set at the Workspace level? In other words, should it be easy for a user to supersede these settings at the Workspace level if a Workbench Admin has configured defaults?
created_at: 2025-03-12T21:12:45Z
author_association: MEMBER
diff_hunk: @@ -217,6 +217,15 @@
             "default": [],
             "markdownDescription": "%r.configuration.interpreters.exclude.markdownDescription%"
           },
+          "positron.r.interpreters.override": {
+            "scope": "window",
path: extensions/positron-r/package.json
user: login: jennybc
body: > Although pinning a version for R could be achieved by specifying a specific interpreter in the override option, it feels like a slightly different affordance to me.

Yeah I agree it almost feels like off-label usage. I'm happy with your existing plan to switch these all to `machine` scope in a later PR and let #2659 continue to track the matter of whether/how to offer such control in a workspace.
created_at: 2025-03-12T21:30:39Z
author_association: MEMBER
diff_hunk: @@ -46,70 +52,15 @@ export enum RRuntimeSource {
  * @param context The extension context.
  */
 export async function* rRuntimeDiscoverer(): AsyncGenerator<positron.LanguageRuntimeMetadata> {
-	const binaries = new Map<string, RBinary>();
-
-	function updateBinaries(binary: RBinary | RBinary[] | undefined): void {
-		if (!binary) {
-			return;
-		}
-		const input = Array.isArray(binary) ? binary : [binary];
-		for (const binary of input) {
-			if (binaries.has(binary.path)) {
-				const reasons = new Set(binaries.get(binary.path)!.reasons.concat(binary.reasons));
-				binaries.get(binary.path)!.reasons = Array.from(reasons);
-			} else {
-				binaries.set(binary.path, binary);
-			}
-		}
-	}
-
-	// Find the current R binary(ies) for various definitions of "current".
-	// The first item here will eventually be marked as The Current R Binary.
-	const currentBinaries = await currentRBinaryCandidates();
-	updateBinaries(currentBinaries);
-
-	// Now we scour the system for all R binaries we can find.
-	const systemHqBinaries = discoverHQBinaries(rHeadquarters());
-	updateBinaries(systemHqBinaries);
-
-	const registryBinaries = await discoverRegistryBinaries();
-	updateBinaries(registryBinaries);
-
-	const moreBinaries = discoverAdHocBinaries([
-		'/usr/bin/R',
-		'/usr/local/bin/R',
-		'/opt/local/bin/R',
-		'/opt/homebrew/bin/R'
-	]);
-	updateBinaries(moreBinaries);
-
-	// Optional, user-specified root directories or binaries
-	const userBinaries = discoverUserSpecifiedBinaries();
-	updateBinaries(userBinaries);
-
-	// Search locations for R binaries that are conventional on servers. These locations are also
-	// searched by RStudio/Posit Workbench on POSIX platforms, such as Linux and macOS.
-	// See https://github.com/rstudio/rstudio/blob/bb8cbf17bb415467f87d6e415f9e3777fa46e583/src/cpp/core/r_util/RVersionsPosix.cpp#L121-L147
-	if (os.platform() !== 'win32') {
-		const serverBinaries = discoverServerBinaries();
-		updateBinaries(serverBinaries);
-	}
-
-	// (Try to) promote each RBinary to a proper RInstallation
-	let rInstallations: Array<RInstallation> = [];
-	if (currentBinaries.length > 0) {
-		const currentBinary = currentBinaries[0];
-		rInstallations.push(new RInstallation(currentBinary.path, true, currentBinary.reasons));
-		binaries.delete(currentBinary.path);
-	}
+	// Discover R binaries on the system
path: extensions/positron-r/src/provider.ts
user: login: jennybc
body: It feels like we gained several unnecessary comments (I think I recognize this behaviour from when I work with co-pilot). And I think we lost some actually useful comments also. I'll point those out.
created_at: 2025-03-13T18:09:57Z
author_association: MEMBER
diff_hunk: @@ -46,70 +52,15 @@ export enum RRuntimeSource {
  * @param context The extension context.
  */
 export async function* rRuntimeDiscoverer(): AsyncGenerator<positron.LanguageRuntimeMetadata> {
-	const binaries = new Map<string, RBinary>();
-
-	function updateBinaries(binary: RBinary | RBinary[] | undefined): void {
-		if (!binary) {
-			return;
-		}
-		const input = Array.isArray(binary) ? binary : [binary];
-		for (const binary of input) {
-			if (binaries.has(binary.path)) {
-				const reasons = new Set(binaries.get(binary.path)!.reasons.concat(binary.reasons));
-				binaries.get(binary.path)!.reasons = Array.from(reasons);
-			} else {
-				binaries.set(binary.path, binary);
-			}
-		}
-	}
-
-	// Find the current R binary(ies) for various definitions of "current".
-	// The first item here will eventually be marked as The Current R Binary.
-	const currentBinaries = await currentRBinaryCandidates();
-	updateBinaries(currentBinaries);
-
-	// Now we scour the system for all R binaries we can find.
-	const systemHqBinaries = discoverHQBinaries(rHeadquarters());
-	updateBinaries(systemHqBinaries);
-
-	const registryBinaries = await discoverRegistryBinaries();
-	updateBinaries(registryBinaries);
-
-	const moreBinaries = discoverAdHocBinaries([
-		'/usr/bin/R',
-		'/usr/local/bin/R',
-		'/opt/local/bin/R',
-		'/opt/homebrew/bin/R'
-	]);
-	updateBinaries(moreBinaries);
-
-	// Optional, user-specified root directories or binaries
-	const userBinaries = discoverUserSpecifiedBinaries();
-	updateBinaries(userBinaries);
-
-	// Search locations for R binaries that are conventional on servers. These locations are also
-	// searched by RStudio/Posit Workbench on POSIX platforms, such as Linux and macOS.
-	// See https://github.com/rstudio/rstudio/blob/bb8cbf17bb415467f87d6e415f9e3777fa46e583/src/cpp/core/r_util/RVersionsPosix.cpp#L121-L147
-	if (os.platform() !== 'win32') {
-		const serverBinaries = discoverServerBinaries();
-		updateBinaries(serverBinaries);
-	}
-
-	// (Try to) promote each RBinary to a proper RInstallation
-	let rInstallations: Array<RInstallation> = [];
-	if (currentBinaries.length > 0) {
-		const currentBinary = currentBinaries[0];
-		rInstallations.push(new RInstallation(currentBinary.path, true, currentBinary.reasons));
-		binaries.delete(currentBinary.path);
-	}
+	// Discover R binaries on the system
+	const { binaries, currentBinary } = await getBinaries();
 
-	binaries.forEach(rbin => {
-		rInstallations.push(new RInstallation(rbin.path, false, rbin.reasons));
-	});
-
-	const rejectedRInstallations: Array<RInstallation> = [];
-	rInstallations = rInstallations
+	// Promote R binaries to R installations
+	const rejectedRInstallations: RInstallation[] = [];
+	const rInstallations: RInstallation[] = binaries
+		.map(rbin => new RInstallation(rbin.path, rbin.path === currentBinary, rbin.reasons))
 		.filter(r => {
+			// Filter out any rejected R installations
path: extensions/positron-r/src/provider.ts
user: login: jennybc
body: Another example of a comment that doesn't seem necessary. I won't point all of these out as it's somewhat subjective, but it would be great if you took a pass at removing some (I think) co-pilot added. In some cases, the code seems pretty self-explanatory already and in others, the new comment feels redundant with more detailed, in-place comments just below (eg. sorting by version number).
created_at: 2025-03-13T18:11:47Z
author_association: MEMBER
diff_hunk: @@ -181,6 +135,57 @@ export async function* rRuntimeDiscoverer(): AsyncGenerator<positron.LanguageRun
 	}
 }
 
+async function getBinaries(): Promise<DiscoveredBinaries> {
+	// If the override paths are specified, use them exclusively
+	const overrideBinaries = discoverOverrideBinaries();
+	if (!!overrideBinaries) {
+		return { binaries: overrideBinaries, currentBinary: undefined };
+	}
+
+	// Consult various sources of R binaries
path: extensions/positron-r/src/provider.ts
user: login: jennybc
body: Here's where I feel like some helpful comments have actually been lost, when this got extracted into a helper (I'm supportive of the helper, to be clear).
created_at: 2025-03-13T18:14:04Z
author_association: MEMBER
diff_hunk: @@ -181,6 +135,57 @@ export async function* rRuntimeDiscoverer(): AsyncGenerator<positron.LanguageRun
 	}
 }
 
+async function getBinaries(): Promise<DiscoveredBinaries> {
+	// If the override paths are specified, use them exclusively
+	const overrideBinaries = discoverOverrideBinaries();
+	if (!!overrideBinaries) {
+		return { binaries: overrideBinaries, currentBinary: undefined };
+	}
+
+	// Consult various sources of R binaries
path: extensions/positron-r/src/provider.ts
user: login: jennybc
body: Update: I now see that some of this got transferred to the docstring above the helper. So now I'm just asking that you confirm that any wisdom in those comments got preserved and landed in what seems like the most useful place for code maintenance. 
created_at: 2025-03-13T18:19:08Z
author_association: MEMBER
diff_hunk: @@ -217,6 +217,15 @@
             "default": [],
             "markdownDescription": "%r.configuration.interpreters.exclude.markdownDescription%"
           },
+          "positron.r.interpreters.override": {
+            "scope": "window",
path: extensions/positron-r/package.json
user: login: melissa-barca
body: On the Workbench side we're hoping to give admins the ability to configure different profiles for users by taking advantage of the Code OSS profiles: https://github.com/rstudio/rstudio-pro/issues/7468
This would be in place of the user profiles admins can currently configure for RStudio sessions: https://docs.posit.co/ide/server-pro/rstudio_pro_sessions/user_and_group_profiles.html#creating-profiles

I'm going to work on getting an exploratory document typed up about this over the next few days, but I think this is more along the lines of what you're looking for @jennybc ?
created_at: 2025-03-13T18:40:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -217,6 +217,15 @@
             "default": [],
             "markdownDescription": "%r.configuration.interpreters.exclude.markdownDescription%"
           },
+          "positron.r.interpreters.override": {
+            "scope": "window",
path: extensions/positron-r/package.json
user: login: jennybc
body: Sounds good!
created_at: 2025-03-13T18:42:53Z
author_association: MEMBER
diff_hunk: @@ -46,70 +52,15 @@ export enum RRuntimeSource {
  * @param context The extension context.
  */
 export async function* rRuntimeDiscoverer(): AsyncGenerator<positron.LanguageRuntimeMetadata> {
-	const binaries = new Map<string, RBinary>();
-
-	function updateBinaries(binary: RBinary | RBinary[] | undefined): void {
-		if (!binary) {
-			return;
-		}
-		const input = Array.isArray(binary) ? binary : [binary];
-		for (const binary of input) {
-			if (binaries.has(binary.path)) {
-				const reasons = new Set(binaries.get(binary.path)!.reasons.concat(binary.reasons));
-				binaries.get(binary.path)!.reasons = Array.from(reasons);
-			} else {
-				binaries.set(binary.path, binary);
-			}
-		}
-	}
-
-	// Find the current R binary(ies) for various definitions of "current".
-	// The first item here will eventually be marked as The Current R Binary.
-	const currentBinaries = await currentRBinaryCandidates();
-	updateBinaries(currentBinaries);
-
-	// Now we scour the system for all R binaries we can find.
-	const systemHqBinaries = discoverHQBinaries(rHeadquarters());
-	updateBinaries(systemHqBinaries);
-
-	const registryBinaries = await discoverRegistryBinaries();
-	updateBinaries(registryBinaries);
-
-	const moreBinaries = discoverAdHocBinaries([
-		'/usr/bin/R',
-		'/usr/local/bin/R',
-		'/opt/local/bin/R',
-		'/opt/homebrew/bin/R'
-	]);
-	updateBinaries(moreBinaries);
-
-	// Optional, user-specified root directories or binaries
-	const userBinaries = discoverUserSpecifiedBinaries();
-	updateBinaries(userBinaries);
-
-	// Search locations for R binaries that are conventional on servers. These locations are also
-	// searched by RStudio/Posit Workbench on POSIX platforms, such as Linux and macOS.
-	// See https://github.com/rstudio/rstudio/blob/bb8cbf17bb415467f87d6e415f9e3777fa46e583/src/cpp/core/r_util/RVersionsPosix.cpp#L121-L147
-	if (os.platform() !== 'win32') {
-		const serverBinaries = discoverServerBinaries();
-		updateBinaries(serverBinaries);
-	}
-
-	// (Try to) promote each RBinary to a proper RInstallation
-	let rInstallations: Array<RInstallation> = [];
-	if (currentBinaries.length > 0) {
-		const currentBinary = currentBinaries[0];
-		rInstallations.push(new RInstallation(currentBinary.path, true, currentBinary.reasons));
-		binaries.delete(currentBinary.path);
-	}
+	// Discover R binaries on the system
path: extensions/positron-r/src/provider.ts
user: login: sharon-wang
body: I added the comments in deliberately, but I have a bias for verbosity and using comments as sort of a "step heading" for chunks of code 😆 I removed some of them and restored any that were missing post-helper-refactor!
created_at: 2025-03-14T15:09:50Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-05T22:04:03Z
body: <!-- PR Tags -->
<!-- 
🚨 RED ALERT! ✋ Rule breaker detected! Tags don’t go here, they belong above ^ in the PR description using this proper format: `@:tag`. Changing them here won't do anything (trust us, we’ve tried). Confused? Check out the README hyperlink.
-->

**E2E Tests** 🚀
This PR will run tests tagged with: `@:critical` `@:interpreters`

<sup>[readme](https://github.com/posit-dev/positron/blob/main/test/e2e/README.md#pull-requests-and-test-tags)</sup>&nbsp;&nbsp;<sup>[valid tags](https://github.com/posit-dev/positron/blob/main/test/e2e/infra/test-runner/test-tags.ts)</sup>
author_association: NONE
user: login: sharon-wang
created_at: 2025-03-12T17:43:44Z
body: > I'd describe this as consulting the overrides inside of some of our existing sub-strategies for R discovery. Which sort of sprinkles this logic around / pushes it down into helpers.
> 
> Did you consider solving from the other direction? Meaning: If overrides are specified, just load up those binaries and bypass all the other sub-strategies. As it stands, it's not obvious to me that we're skipping over, e.g., the registry search and ad hoc binaries. (I realize the runtime discoverer is not really organized favorably for this approach right now, but perhaps we should at least discuss a future refactoring that makes things more obvious.)

I was aiming for minimal lift in my original approach, which has the effect you described. After looking at the code, I think the reorganization in https://github.com/posit-dev/positron/pull/6655/commits/37adb983720746406f95ffe14cf2a2a59dc08442 might be ok! I moved a bunch of things around, so happy to do a virtual "desk-side" review if that's easier!

I reorganized things to favour the overrides and skip over everything else if provided. I also moved the interpreter settings code to a separate file.
author_association: MEMBER
user: login: jennybc
created_at: 2025-03-13T18:27:20Z
body: I definitely think this refactoring, inspired by the need to insert the overrides, is a good move!
author_association: MEMBER
user: login: sharon-wang
created_at: 2025-03-14T15:11:45Z
body: Latest changes:

- additional handling for a few more override scenarios [a732f81](https://github.com/posit-dev/positron/pull/6655/commits/a732f81a520bfa1a24cc5422feee1327963376f3)
- I've added in a debugging command `r.interpreters.settingsInfo` which prints the interpreter settings to R Language Pack Output [59a1f19](https://github.com/posit-dev/positron/pull/6655/commits/59a1f19472192a16b60fcb159b1b2028d958f02f) (I added a similar one for Python already)
- some comment pruning and gardening [d7a43ba](https://github.com/posit-dev/positron/pull/6655/commits/d7a43ba8d2558671333042d4dbf3a739cfd338dc)

Will merge once checks are green!
author_association: MEMBER