From 83f70a2335f0fd71e3076daa84bd8491bbb137ef Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Wed, 22 Jun 2022 16:54:51 +1000 Subject: [PATCH 01/22] R executable selection --- package.json | 5 + src/executables/executable.ts | 63 ++++++++++ src/executables/index.ts | 106 ++++++++++++++++ src/executables/locator/index.ts | 19 +++ src/executables/locator/shared.ts | 36 ++++++ src/executables/locator/unix.ts | 81 ++++++++++++ src/executables/locator/windows.ts | 104 ++++++++++++++++ src/executables/service.ts | 144 +++++++++++++++++++++ src/executables/storage.ts | 36 ++++++ src/executables/ui/index.ts | 7 ++ src/executables/ui/quickpick.ts | 194 +++++++++++++++++++++++++++++ src/executables/ui/status.ts | 58 +++++++++ src/extension.ts | 9 +- src/rTerminal.ts | 3 +- src/util.ts | 9 +- yarn.lock | 6 +- 16 files changed, 872 insertions(+), 8 deletions(-) create mode 100644 src/executables/executable.ts create mode 100644 src/executables/index.ts create mode 100644 src/executables/locator/index.ts create mode 100644 src/executables/locator/shared.ts create mode 100644 src/executables/locator/unix.ts create mode 100644 src/executables/locator/windows.ts create mode 100644 src/executables/service.ts create mode 100644 src/executables/storage.ts create mode 100644 src/executables/ui/index.ts create mode 100644 src/executables/ui/quickpick.ts create mode 100644 src/executables/ui/status.ts diff --git a/package.json b/package.json index eb155c315..5915912e7 100644 --- a/package.json +++ b/package.json @@ -289,6 +289,11 @@ } ], "commands": [ + { + "category": "R", + "command": "r.setExecutable", + "title": "Select executable" + }, { "command": "r.workspaceViewer.refreshEntry", "title": "Manual Refresh", diff --git a/src/executables/executable.ts b/src/executables/executable.ts new file mode 100644 index 000000000..4d85d02fd --- /dev/null +++ b/src/executables/executable.ts @@ -0,0 +1,63 @@ +import { getVersionFromPath, getArchitectureFromPath } from './locator'; + +export class RExecutableFactory { + static createExecutable(executablePath: string): RExecutable { + if (new RegExp('\\.conda').exec(executablePath)?.length > 0) { + return new VirtualRExecutable(executablePath); + } else { + return new RExecutable(executablePath); + } + } + + private constructor() { + // + } +} + +export class RExecutable { + private _rBin: string; + private _rVersion: string; + private _arch: string; + + constructor(bin_path: string) { + this._rBin = bin_path; + this._rVersion = getVersionFromPath(bin_path); + this._arch = getArchitectureFromPath(bin_path); + } + + public get rBin(): string { + return this._rBin; + } + + public get rVersion(): string { + return this._rVersion; + } + + public get rArch(): string { + return this._arch; + } + + public get tooltip(): string { + return `R ${this.rVersion} ${this.rArch}`; + } +} + +export class VirtualRExecutable extends RExecutable { + constructor(bin_path: string) { + super(bin_path); + } + + public get name(): string { + const reg = new RegExp('(?<=\\/envs\\/)(.*?)(?=\\/)'); + return reg.exec(this.rBin)[0]; + } + + public get tooltip(): string { + return `${this.name} (R ${this.rVersion} ${this.rArch})`; + } + + // todo, hardcoded + public get activationCommand(): string[] { + return ['activate', this.name]; + } +} diff --git a/src/executables/index.ts b/src/executables/index.ts new file mode 100644 index 000000000..de56eb1f2 --- /dev/null +++ b/src/executables/index.ts @@ -0,0 +1,106 @@ +import path = require('path'); +import * as fs from 'fs-extra'; +import * as vscode from 'vscode'; + +import { LocatorServiceFactory, AbstractLocatorService } from './locator'; +import { ExecutableStatusItem, ExecutableQuickPick } from './ui'; +import { RExecutableService, WorkspaceExecutableEvent } from './service'; +import { extensionContext } from '../extension'; +import { spawnAsync } from '../util'; +import { RExecutable, VirtualRExecutable } from './executable'; + +// super class that manages relevant sub classes +export class RExecutableManager { + private retrievalService: AbstractLocatorService; + private statusBar: ExecutableStatusItem; + private quickPick: ExecutableQuickPick; + private executableService: RExecutableService; + + constructor() { + this.retrievalService = LocatorServiceFactory.getLocator(); + this.retrievalService.refreshPaths(); + this.executableService = new RExecutableService(); + this.statusBar = new ExecutableStatusItem(this.executableService); + this.quickPick = new ExecutableQuickPick(this.executableService, this.retrievalService); + + extensionContext.subscriptions.push( + this.onDidChangeActiveExecutable(() => { + this.reload(); + }), + vscode.window.onDidChangeActiveTextEditor((e: vscode.TextEditor) => { + if (e?.document) { + this.reload(); + } + }), + this.executableService, + this.statusBar, + this.quickPick + ); + this.reload(); + } + + public get activeExecutable(): RExecutable { + return this.executableService.activeExecutable; + } + + public get executableQuickPick(): ExecutableQuickPick { + return this.quickPick; + } + + public get executableStatusItem(): ExecutableStatusItem { + return this.statusBar; + } + + public get onDidChangeActiveExecutable(): vscode.Event { + return this.executableService.onDidChangeActiveExecutable; + } + + public get onDidChangeWorkspaceExecutable(): vscode.Event { + return this.executableService.onDidChangeWorkspaceExecutable; + } + + /** + * @description + * Orders a refresh of the executable manager, causing a refresh of the language status bar item and + * activates a conda environment if present. + * @memberof RExecutableManager + */ + public reload(): void { + this.statusBar.refresh(); + const loading = this.activateEnvironment(); + void this.statusBar.busy(loading); + } + + private async activateEnvironment(): Promise { + const opts = { + env: { + ...process.env + }, + }; + if (this.activeExecutable instanceof VirtualRExecutable && opts.env.CONDA_DEFAULT_ENV !== this.activeExecutable.name) { + return spawnAsync( + 'conda', // hard coded for now + this.activeExecutable.activationCommand, + opts, + undefined + ); + } else { + return Promise.resolve(); + } + } + +} + + +/** + * Is the folder of a given executable a valid R installation? + * + * A path is valid if the folder contains the R executable and an Rcmd file. + * @param execPath + * @returns boolean + */ +export function validateRFolder(execPath: string): boolean { + const basename = process.platform === 'win32' ? 'R.exe' : 'R'; + const scriptPath = path.normalize(`${execPath}/../Rcmd`); + return fs.existsSync(execPath) && path.basename(basename) && fs.existsSync(scriptPath); +} diff --git a/src/executables/locator/index.ts b/src/executables/locator/index.ts new file mode 100644 index 000000000..ccdd6339a --- /dev/null +++ b/src/executables/locator/index.ts @@ -0,0 +1,19 @@ +export * from './shared'; + +import { UnixExecLocator } from './unix'; +import { WindowsExecLocator } from './windows'; +import { AbstractLocatorService } from './shared'; + +export class LocatorServiceFactory { + static getLocator(): AbstractLocatorService { + if (process.platform === 'win32') { + return new WindowsExecLocator(); + } else { + return new UnixExecLocator(); + } + } + + private constructor() { + // + } +} \ No newline at end of file diff --git a/src/executables/locator/shared.ts b/src/executables/locator/shared.ts new file mode 100644 index 000000000..87044cfc2 --- /dev/null +++ b/src/executables/locator/shared.ts @@ -0,0 +1,36 @@ +import path = require('path'); +import * as fs from 'fs-extra'; + +export function getVersionFromPath(rPath: string): string { + if (process.platform === 'win32') { + // not sure how to do this + return ''; + } else { + try { + const scriptPath = path.normalize(`${rPath}/../Rcmd`); + const rCmdFile = fs.readFileSync(scriptPath, 'utf-8'); + const regex = /(?<=R_VERSION=)[0-9.]*/g; + const version = regex.exec(rCmdFile)?.[0]; + return version ?? ''; + } catch (error) { + return ''; + } + } +} + +export function getArchitectureFromPath(path: string): string { + if (process.platform === 'win32') { + // \\bin\\i386 = 32bit + // \\bin\\x64 = 64bit + return ''; + } else { + return '64-bit'; + } +} + +export abstract class AbstractLocatorService { + protected binary_paths: string[]; + public abstract get hasBinaries(): boolean; + public abstract get binaries(): string[]; + public abstract refreshPaths(): void; +} diff --git a/src/executables/locator/unix.ts b/src/executables/locator/unix.ts new file mode 100644 index 000000000..4e47bc800 --- /dev/null +++ b/src/executables/locator/unix.ts @@ -0,0 +1,81 @@ +import * as fs from 'fs-extra'; +import * as os from 'os'; +import path = require('path'); + +import { AbstractLocatorService } from './shared'; + +export class UnixExecLocator extends AbstractLocatorService { + public get hasBinaries(): boolean { + return this.binary_paths.length > 0; + } + public get binaries(): string[] { + return this.binary_paths; + } + public refreshPaths(): void { + this.binary_paths = Array.from( + new Set([ + ...this.getHomeFromDirs(), + ...this.getHomeFromEnv(), + ... this.getHomeFromConda() + ]) + ); + } + + private potential_bin_paths: string[] = [ + '/usr/lib64/R/bin/R', + '/usr/lib/R/bin/R', + '/usr/local/lib64/R/bin/R', + '/usr/local/lib/R/bin/R', + '/opt/local/lib64/R/bin/R', + '/opt/local/lib/R/bin/R' + ]; + + private getHomeFromDirs(): string[] { + const dirBins: string[] = []; + for (const bin of this.potential_bin_paths) { + if (fs.existsSync(bin)) { + dirBins.push(bin); + } + } + return dirBins; + } + + private getHomeFromConda(): string[] { + const dirBins: string[] = []; + const conda_dirs = [ + `${os.homedir()}/.conda/environments.txt` + ]; + for (const dir of conda_dirs) { + if (fs.existsSync(dir)) { + const lines = fs.readFileSync(dir).toString(); + for (const line of lines.split('\n')) { + if (line) { + const potential_dirs = [ + `${line}/lib64/R/bin/R`, + `${line}/lib/R/bin/R` + ]; + for (const dir of potential_dirs) { + if (fs.existsSync(dir)) { + dirBins.push(dir); + } + } + } + } + } + } + return dirBins; + } + + private getHomeFromEnv(): string[] { + const envBins: string[] = []; + const os_paths: string[] | string = process.env.PATH.split(';'); + + for (const os_path of os_paths) { + const os_r_path: string = path.join(os_path, 'R'); + if (fs.existsSync(os_r_path)) { + envBins.push(os_r_path); + } + } + return envBins; + } +} diff --git a/src/executables/locator/windows.ts b/src/executables/locator/windows.ts new file mode 100644 index 000000000..9c7a515f8 --- /dev/null +++ b/src/executables/locator/windows.ts @@ -0,0 +1,104 @@ +import * as fs from 'fs-extra'; +import * as os from 'os'; +import path = require('path'); +import winreg = require('winreg'); + +import { AbstractLocatorService } from './shared'; + +export class WindowsExecLocator extends AbstractLocatorService { + public get hasBinaries(): boolean { + return this.binary_paths.length > 0; + } + public get binaries(): string[] { + return this.binary_paths; + } + public refreshPaths(): void { + this.binary_paths = Array.from( + new Set([ + ...this.getHomeFromDirs(), + ...this.getHomeFromEnv(), + ...this.getHomeFromRegistry(), + // ... this.getHomeFromConda() + ]) + ); + } + + private potential_bin_paths: string[] = [ + '%ProgramFiles%\\R\\', + '%ProgramFiles(x86)%\\R\\' + ]; + + private getHomeFromRegistry(): string[] { + const registryBins: string[] = []; + const potentialBins = [ + new winreg({ + hive: winreg.HKLM, + key: '\\SOFTWARE\\R-core\\R', + }), + new winreg({ + hive: winreg.HKLM, + key: '\\SOFTWARE\\R-core\\R64', + }) + ]; + + for (const bin of potentialBins) { + void new Promise( + (c, e) => { + bin.get('InstallPath', (err, result) => err === null ? c(result) : e(err)); + } + ).then((item: winreg.RegistryItem) => { + if (item) { + const resolvedBin = item.value; + const i386 = `${resolvedBin}\\i386\\`; + const x64 = `${resolvedBin}\\x64\\`; + + if (fs.existsSync(i386)) { + registryBins.push(i386); + } + + if (fs.existsSync(x64)) { + registryBins.push(x64); + } + } + }); + } + + return registryBins; + } + + private getHomeFromDirs(): string[] { + const dirBins: string[] = []; + for (const bin of this.potential_bin_paths) { + const resolvedBin = path.resolve(bin); + if (fs.existsSync(resolvedBin)) { + const i386 = `${resolvedBin}\\i386\\`; + const x64 = `${resolvedBin}\\x64\\`; + + if (fs.existsSync(i386)) { + dirBins.push(i386); + } + + if (fs.existsSync(x64)) { + dirBins.push(x64); + } + } + } + return dirBins; + } + + private getHomeFromEnv(): string[] { + const envBins: string[] = []; + const os_paths: string[] | string = process.env.PATH.split(';'); + + for (const os_path of os_paths) { + const os_r_path: string = path.join(os_path, 'R' + '.exe'); + if (fs.existsSync(os_r_path)) { + envBins.push(os_r_path); + } + } + return envBins; + } + + // todo + // private getHomeFromConda() {} +} diff --git a/src/executables/service.ts b/src/executables/service.ts new file mode 100644 index 000000000..bdb9ce937 --- /dev/null +++ b/src/executables/service.ts @@ -0,0 +1,144 @@ +import * as vscode from 'vscode'; + +import { validateRFolder } from '.'; +import { RExecutable, RExecutableFactory } from './executable'; +import { config, getCurrentWorkspaceFolder, getRPathConfigEntry } from '../util'; +import { clearExecutable, getExecutable, getExecutableStore, storeExecutable } from './storage'; + +/** + * @description + * @export + * @interface WorkspaceExecutableEvent + */ +export interface WorkspaceExecutableEvent { + workingFolder: vscode.WorkspaceFolder, + executable: RExecutable +} + +/** + * @description vf + * @export + * @class RExecutableService + * @implements {vscode.Disposable} + */ +export class RExecutableService implements vscode.Disposable { + private executableEmitter: vscode.EventEmitter; + private workspaceEmitter: vscode.EventEmitter; + private executables: Map; + + /** + * Creates an instance of RExecutableService. + * @memberof RExecutableService + */ + public constructor() { + this.executableEmitter = new vscode.EventEmitter(); + this.workspaceEmitter = new vscode.EventEmitter(); + this.executables = new Map(); + + const confPath = config().get(getRPathConfigEntry()); + for (const [dirPath, execPath] of getExecutableStore()) { + this.executables.set(dirPath, RExecutableFactory.createExecutable(execPath)); + } + + if (!getExecutable(getCurrentWorkspaceFolder().uri.fsPath) && confPath && validateRFolder(confPath)) { + console.log(`[RExecutableService] Executable set to configuration path: ${confPath}`); + const exec = RExecutableFactory.createExecutable(confPath); + this.activeExecutable = exec; + } + } + + + /** + * @description + * @memberof RExecutableService + */ + public set activeExecutable(executable: RExecutable) { + if (executable === null) { + this.executables.delete(getCurrentWorkspaceFolder().uri.fsPath); + clearExecutable(getCurrentWorkspaceFolder().uri.fsPath); + console.log('[RExecutableService] executable cleared'); + this.executableEmitter.fire(null); + } else if (this.activeExecutable !== executable) { + this.executables.set(getCurrentWorkspaceFolder().uri.fsPath, executable); + storeExecutable(executable.rBin, getCurrentWorkspaceFolder().uri.fsPath); + console.log('[RExecutableService] executable changed'); + this.executableEmitter.fire(executable); + } + } + + /** + * @description + * Returns the current *active* R executable. + * This may differ depending on the current active workspace folder. + * @type {RExecutable} + * @memberof RExecutableService + */ + public get activeExecutable(): RExecutable { + const currWorkspacePath = getCurrentWorkspaceFolder().uri.fsPath; + if (currWorkspacePath) { + return this.executables.get(currWorkspacePath); + } else { + return this.executables.get(vscode.window.activeTextEditor.document.uri.fsPath); + } + } + + /** + * @description + * Set the R executable associated with a given workspace folder. + * @param {string} folder + * @param {RExecutable} executable + * @memberof RExecutableService + */ + public setWorkspaceExecutable(folder: string, executable: RExecutable): void { + if (this.executables.get(folder) !== executable) { + const workspaceFolderUri = vscode.Uri.file(folder); + this.workspaceEmitter.fire({ workingFolder: vscode.workspace.getWorkspaceFolder(workspaceFolderUri), executable: executable }); + storeExecutable(executable.rBin, folder); + } + this.executables.set(folder, executable); + this.executableEmitter.fire(executable); + } + + /** + * @description + * Get the R executable associated with a given workspace folder. + * @param {string} folder + * @returns {*} {RExecutable} + * @memberof RExecutableService + */ + public getWorkspaceExecutable(folder: string): RExecutable { + return this.executables.get(folder); + } + + /** + * @description + * An event that is fired whenever the active executable changes. + * This can occur, for instance, when changing focus between multi-root workspaces. + * @readonly + * @type {vscode.Event} + * @memberof RExecutableService + */ + public get onDidChangeActiveExecutable(): vscode.Event { + return this.executableEmitter.event; + } + + /** + * @description + * Event that is triggered when the executable associated with a workspace is changed. + * @readonly + * @type {vscode.Event} + * @memberof RExecutableService + */ + public get onDidChangeWorkspaceExecutable(): vscode.Event { + return this.workspaceEmitter.event; + } + + /** + * @description + * @memberof RExecutableService + */ + public dispose(): void { + this.executableEmitter.dispose(); + this.workspaceEmitter.dispose(); + } +} diff --git a/src/executables/storage.ts b/src/executables/storage.ts new file mode 100644 index 000000000..bab8fd23c --- /dev/null +++ b/src/executables/storage.ts @@ -0,0 +1,36 @@ +import { extensionContext } from '../extension'; + +/** + * Executables are stored as a map of working directories and bin paths + */ +export function getExecutableStore(): Map { + return stringToMap(extensionContext.globalState.get('rExecMap', '')); +} + +export function getExecutable(workingDir: string): string { + const store: Map = getExecutableStore(); + return store.get(workingDir); +} + +export function storeExecutable(binPath: string, workingDir: string): void { + const currentStore: Map = getExecutableStore(); + currentStore.set(workingDir, binPath); + void extensionContext.globalState.update('rExecMap', mapToString(currentStore)); +} + +export function clearExecutable(workingDir: string): boolean { + const currentStore: Map = getExecutableStore(); + return currentStore.delete(workingDir); +} + +function mapToString(map: Map): string { + return JSON.stringify([...map]); +} + +function stringToMap(str: string): Map { + try { + return new Map(JSON.parse(str)); + } catch (error) { + return new Map(); + } +} \ No newline at end of file diff --git a/src/executables/ui/index.ts b/src/executables/ui/index.ts new file mode 100644 index 000000000..91e4545af --- /dev/null +++ b/src/executables/ui/index.ts @@ -0,0 +1,7 @@ +export { ExecutableQuickPick } from './quickpick'; +export { ExecutableStatusItem } from './status'; + +export enum ExecutableNotifications { + badFolder = 'Supplied R executable directory is not a valid R directory.', + badConfig = 'Configured path is not a valid R executable directory.' +} \ No newline at end of file diff --git a/src/executables/ui/quickpick.ts b/src/executables/ui/quickpick.ts new file mode 100644 index 000000000..0a9ffd60c --- /dev/null +++ b/src/executables/ui/quickpick.ts @@ -0,0 +1,194 @@ +import * as vscode from 'vscode'; + +import { ExecutableNotifications } from '.'; +import { validateRFolder } from '..'; +import { config, getCurrentWorkspaceFolder, getRPathConfigEntry } from '../../util'; +import { RExecutable, RExecutableFactory, VirtualRExecutable } from '../executable'; +import { AbstractLocatorService } from '../locator'; +import { RExecutableService } from '../service'; + + +const confRpath = () => { + return config().get(getRPathConfigEntry()); +}; + +class ExecutableQuickPickItem implements vscode.QuickPickItem { + public label: string; + public description: string; + public detail?: string; + public picked?: boolean; + public alwaysShow?: boolean; + + private _executable: RExecutable = undefined; + + constructor(executable: RExecutable) { + this._executable = executable; + this.label = executable.tooltip; + this.description = executable.rBin; + } + + public get executable(): RExecutable { + return this._executable; + } + +} + +enum PathQuickPickMenu { + search = '$(plus) Enter R binary path...', + configuration = '$(settings-gear) Configuration path' +} + +export class ExecutableQuickPick implements vscode.Disposable { + private qp: vscode.QuickPick; + private retriever: AbstractLocatorService; + private service: RExecutableService; + private currentFolder: string; + + public constructor(service: RExecutableService, retriever: AbstractLocatorService) { + this.service = service; + this.retriever = retriever; + this.currentFolder = getCurrentWorkspaceFolder().uri.fsPath; + } + + public dispose(): void { + this.qp.dispose(); + } + + private setItems(): void { + function sortBins(bins: RExecutable[]) { + return bins.sort((a, b) => { + if (a instanceof RExecutable && b instanceof VirtualRExecutable) { + return 1; + } else if (b instanceof RExecutable && a instanceof VirtualRExecutable) { + return -1; + } else { + return a.rVersion.localeCompare(b.rVersion, undefined, { numeric: true, sensitivity: 'base' }); + } + }); + } + const qpItems: vscode.QuickPickItem[] = []; + const executables: RExecutable[] = []; + const configPath = confRpath(); + qpItems.push( + { + label: PathQuickPickMenu.search, + alwaysShow: true, + picked: false + }, + { + label: PathQuickPickMenu.configuration, + alwaysShow: true, + description: configPath, + detail: validateRFolder(configPath) ? '' : 'Invalid R folder', + picked: false + } + ); + + this.retriever.binaries.forEach(home => { + if (validateRFolder(home)) { + const inst = RExecutableFactory.createExecutable(home); + executables.push(inst); + } + }); + sortBins(executables).forEach((bin: RExecutable) => { + qpItems.push(new ExecutableQuickPickItem(bin)); + }); + + this.qp.items = qpItems; + for (const item of this.qp.items) { + if (item.description === this.service.getWorkspaceExecutable(this.currentFolder)?.rBin) { + this.qp.activeItems = [item]; + } + } + } + + public async showQuickPick(): Promise { + function setupQuickpickOpts(self: ExecutableQuickPick): void { + self.qp = vscode.window.createQuickPick(); + self.qp.title = 'Select R executable path'; + self.qp.canSelectMany = false; + self.qp.ignoreFocusOut = true; + self.qp.matchOnDescription = true; + self.qp.placeholder = ''; + self.qp.buttons = [ + { iconPath: new vscode.ThemeIcon('clear-all'), tooltip: 'Clear stored path' }, + { iconPath: new vscode.ThemeIcon('refresh'), tooltip: 'Refresh paths' } + ]; + } + + function setupQuickpickListeners(self: ExecutableQuickPick, resolver: () => void): void { + self.qp.onDidTriggerButton((item: vscode.QuickInputButton) => { + if (item.tooltip === 'Refresh paths') { + self.setItems(); + self.qp.show(); + } else { + self.service.setWorkspaceExecutable(self.currentFolder, null); + self.qp.hide(); + } + }); + self.qp.onDidChangeSelection((items: vscode.QuickPickItem[]) => { + const qpItem = items[0]; + if (qpItem.label) { + switch (qpItem.label) { + case PathQuickPickMenu.search: { + const opts: vscode.OpenDialogOptions = { + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + title: ' R executable file' + }; + void vscode.window.showOpenDialog(opts).then((exec_path) => { + if (validateRFolder(exec_path[0].fsPath)) { + const rExec = RExecutableFactory.createExecutable(exec_path[0].fsPath); + self.service.setWorkspaceExecutable(self.currentFolder, rExec); + } else { + void vscode.window.showErrorMessage(ExecutableNotifications.badFolder); + self.service.setWorkspaceExecutable(self.currentFolder, null); + } + }); + break; + } + case PathQuickPickMenu.configuration: { + if (validateRFolder(confRpath())) { + const rExec = RExecutableFactory.createExecutable(confRpath()); + self.service.setWorkspaceExecutable(self.currentFolder, rExec); + } else { + void vscode.window.showErrorMessage(ExecutableNotifications.badConfig); + self.service.setWorkspaceExecutable(self.currentFolder, null); + } + break; + } + default: { + self.service.setWorkspaceExecutable(self.currentFolder, (qpItem as ExecutableQuickPickItem).executable); + break; + } + } + } + self.qp.hide(); + resolver(); + }); + } + + return await new Promise((res) => { + setupQuickpickOpts(this); + setupQuickpickListeners(this, res); + void showWorkspaceFolderQP().then((folder: vscode.WorkspaceFolder) => { + this.currentFolder = folder.uri.fsPath; + this.setItems(); + this.qp.show(); + }); + }); + } +} + +async function showWorkspaceFolderQP() { + const opts: vscode.WorkspaceFolderPickOptions = { + ignoreFocusOut: true, + placeHolder: 'Select a workspace folder to define an R path for' + }; + if (vscode.workspace.workspaceFolders.length > 1) { + return await vscode.window.showWorkspaceFolderPick(opts); + } else { + return vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor.document.uri); + } +} \ No newline at end of file diff --git a/src/executables/ui/status.ts b/src/executables/ui/status.ts new file mode 100644 index 000000000..8ecbbe2f0 --- /dev/null +++ b/src/executables/ui/status.ts @@ -0,0 +1,58 @@ +import * as vscode from 'vscode'; + +import { VirtualRExecutable } from '../executable'; +import { RExecutableService } from '../service'; + +enum BinText { + name = 'R Language Indicator', + missing = '$(warning) Select executable' +} + +export class ExecutableStatusItem implements vscode.Disposable { + private service: RExecutableService; + private languageStatusItem: vscode.LanguageStatusItem; + + private createItem(): vscode.LanguageStatusItem { + this.languageStatusItem = vscode.languages.createLanguageStatusItem('R Executable Selector', ['r', 'rmd']); + this.languageStatusItem.name = 'R Language Service'; + this.languageStatusItem.command = { + 'title': 'Select R executable', + 'command': 'r.setExecutable' + }; + this.refresh(); + return this.languageStatusItem; + } + + public constructor(service: RExecutableService) { + this.service = service; + this.createItem(); + } + + public refresh(): void { + const execState = this.service.activeExecutable; + if (execState) { + this.languageStatusItem.severity = vscode.LanguageStatusSeverity.Information; + this.languageStatusItem.detail = execState.rBin; + if (execState instanceof VirtualRExecutable) { + this.languageStatusItem.text = `${execState.name} (${execState.rVersion})`; + } else { + this.languageStatusItem.text = execState.rVersion; + } + } else { + this.languageStatusItem.severity = vscode.LanguageStatusSeverity.Warning; + this.languageStatusItem.text = BinText.missing; + this.languageStatusItem.detail = ''; + } + } + + public async busy(prom: Promise): Promise { + this.languageStatusItem.busy = true; + await prom; + this.languageStatusItem.busy = false; + } + + public dispose(): void { + this.languageStatusItem.dispose(); + } + +} diff --git a/src/extension.ts b/src/extension.ts index 622f3cfe7..fa1e2710c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -23,6 +23,7 @@ import * as rShare from './liveShare'; import * as httpgdViewer from './plotViewer'; import * as languageService from './languageService'; import { RTaskProvider } from './tasks'; +import * as rExec from './executables'; // global objects used in other files @@ -35,6 +36,7 @@ export let enableSessionWatcher: boolean = undefined; export let globalHttpgdManager: httpgdViewer.HttpgdManager | undefined = undefined; export let rmdPreviewManager: rmarkdown.RMarkdownPreviewManager | undefined = undefined; export let rmdKnitManager: rmarkdown.RMarkdownKnitManager | undefined = undefined; +export let rExecService: rExec.RExecutableManager | undefined = undefined; // Called (once) when the extension is activated export async function activate(context: vscode.ExtensionContext): Promise { @@ -50,6 +52,7 @@ export async function activate(context: vscode.ExtensionContext): Promise('sessionWatcher'); @@ -61,6 +64,7 @@ export async function activate(context: vscode.ExtensionContext): Promise rExecService.executableQuickPick.showQuickPick(), // run code from editor in terminal 'r.nrow': () => rTerminal.runSelectionOrWord(['nrow']), @@ -69,7 +73,7 @@ export async function activate(context: vscode.ExtensionContext): Promise rTerminal.runSelectionOrWord(['t', 'head']), 'r.names': () => rTerminal.runSelectionOrWord(['names']), 'r.runSource': () => { void rTerminal.runSource(false); }, - 'r.runSelection': (code?: string) => { code ? void rTerminal.runTextInTerm(code) : void rTerminal.runSelection(); }, + 'r.runSelection': (code?: string) => { code ? void rTerminal.runTextInTerm(code) : void rTerminal.runSelection(); }, 'r.runFromLineToEnd': rTerminal.runFromLineToEnd, 'r.runFromBeginningToLine': rTerminal.runFromBeginningToLine, 'r.runSelectionRetainCursor': rTerminal.runSelectionRetainCursor, @@ -108,7 +112,7 @@ export async function activate(context: vscode.ExtensionContext): Promise rmarkdown.newDraft(), - 'r.newFileDocument': () => vscode.workspace.openTextDocument({language: 'r'}).then((v) => vscode.window.showTextDocument(v)), + 'r.newFileDocument': () => vscode.workspace.openTextDocument({ language: 'r' }).then((v) => vscode.window.showTextDocument(v)), // editor independent commands 'r.createGitignore': rGitignore.createGitignore, @@ -251,6 +255,5 @@ export async function activate(context: vscode.ExtensionContext): Promise { VSCODE_WATCHER_DIR: homeExtDir() }; } + termOptions.env['R_BINARY'] = rExecService?.activeExecutable?.rBin; return termOptions; } diff --git a/src/util.ts b/src/util.ts index 883bdf978..53df73f46 100644 --- a/src/util.ts +++ b/src/util.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import * as cp from 'child_process'; import { rGuestService, isGuestSession } from './liveShare'; -import { extensionContext } from './extension'; +import { extensionContext, rExecService } from './extension'; export function config(): vscode.WorkspaceConfiguration { return vscode.workspace.getConfiguration('r'); @@ -68,6 +68,13 @@ export function getRPathConfigEntry(term: boolean = false): string { } export async function getRpath(quote = false, overwriteConfig?: string): Promise { + const execPath = rExecService.activeExecutable?.rBin; + if (execPath) { + return execPath; + } else { + return ''; + } + let rpath = ''; // try the config entry specified in the function arg: diff --git a/yarn.lock b/yarn.lock index 511786385..e4d68719f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -254,9 +254,9 @@ integrity sha512-dchbFCWfVgUSWEvhOkXGS7zjm+K7jCUvGrQkAHPk2Fmslfofp4HQTH2pqnQ3Pw5GPYv0zWa2AQjKtsfZThuemQ== "@types/vscode@^1.60.0": - version "1.60.0" - resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.60.0.tgz#9330c317691b4f53441a18b598768faeeb71618a" - integrity sha512-wZt3VTmzYrgZ0l/3QmEbCq4KAJ71K3/hmMQ/nfpv84oH8e81KKwPEoQ5v8dNCxfHFVJ1JabHKmCvqdYOoVm1Ow== + version "1.68.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.68.0.tgz#c0864e4ea43c509bfc6b53f4e91aa923fd0475b8" + integrity sha512-duBwEK5ta/eBBMJMQ7ECMEsMvlE3XJdRGh3xoS1uOO4jl2Z4LPBl5vx8WvBP10ERAgDRmIt/FaSD4RHyBGbChw== "@types/winreg@^1.2.31": version "1.2.31" From ce25dc3ff558816fe13673b5e5de06cf095fa930 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Wed, 6 Jul 2022 12:10:45 +1000 Subject: [PATCH 02/22] R --version & reduced process creation - Use R --version for details - Only create executables when calling refreshPaths() --- src/executables/executable.ts | 7 +++-- src/executables/locator/shared.ts | 48 ++++++++++++------------------ src/executables/locator/unix.ts | 45 ++++++++++++++++++---------- src/executables/locator/windows.ts | 37 +++++++++++++++-------- src/executables/ui/quickpick.ts | 6 ++-- 5 files changed, 81 insertions(+), 62 deletions(-) diff --git a/src/executables/executable.ts b/src/executables/executable.ts index 4d85d02fd..cd294c09c 100644 --- a/src/executables/executable.ts +++ b/src/executables/executable.ts @@ -1,4 +1,4 @@ -import { getVersionFromPath, getArchitectureFromPath } from './locator'; +import { getRDetailsFromPath } from './locator'; export class RExecutableFactory { static createExecutable(executablePath: string): RExecutable { @@ -20,9 +20,10 @@ export class RExecutable { private _arch: string; constructor(bin_path: string) { + const details = getRDetailsFromPath(bin_path); this._rBin = bin_path; - this._rVersion = getVersionFromPath(bin_path); - this._arch = getArchitectureFromPath(bin_path); + this._rVersion = details.version; + this._arch = details.arch; } public get rBin(): string { diff --git a/src/executables/locator/shared.ts b/src/executables/locator/shared.ts index 87044cfc2..55d25cfce 100644 --- a/src/executables/locator/shared.ts +++ b/src/executables/locator/shared.ts @@ -1,36 +1,26 @@ -import path = require('path'); -import * as fs from 'fs-extra'; +import { execSync } from 'child_process'; +import { RExecutable } from '../executable'; -export function getVersionFromPath(rPath: string): string { - if (process.platform === 'win32') { - // not sure how to do this - return ''; - } else { - try { - const scriptPath = path.normalize(`${rPath}/../Rcmd`); - const rCmdFile = fs.readFileSync(scriptPath, 'utf-8'); - const regex = /(?<=R_VERSION=)[0-9.]*/g; - const version = regex.exec(rCmdFile)?.[0]; - return version ?? ''; - } catch (error) { - return ''; - } - } -} - -export function getArchitectureFromPath(path: string): string { - if (process.platform === 'win32') { - // \\bin\\i386 = 32bit - // \\bin\\x64 = 64bit - return ''; - } else { - return '64-bit'; +export function getRDetailsFromPath(rPath: string): {version: string, arch: string} { + try { + const child = execSync(`${rPath} --version`).toString(); + const versionRegex = /(?<=R version\s)[0-9.]*/g; + const archRegex = /[0-9]*-bit/g; + const out = { + version: child.match(versionRegex)?.[0] ?? '', + arch: child.match(archRegex)?.[0] ?? '' + }; + return out; + } catch (error) { + return { version: '', arch: '' }; } } export abstract class AbstractLocatorService { - protected binary_paths: string[]; - public abstract get hasBinaries(): boolean; - public abstract get binaries(): string[]; + protected _binaryPaths: string[]; + protected _executables: RExecutable[]; + public abstract get hasExecutables(): boolean; + public abstract get executables(): RExecutable[]; + public abstract get binaryPaths(): string[]; public abstract refreshPaths(): void; } diff --git a/src/executables/locator/unix.ts b/src/executables/locator/unix.ts index 4e47bc800..358a0f5c7 100644 --- a/src/executables/locator/unix.ts +++ b/src/executables/locator/unix.ts @@ -1,38 +1,53 @@ import * as fs from 'fs-extra'; import * as os from 'os'; import path = require('path'); +import { RExecutable, RExecutableFactory } from '../executable'; import { AbstractLocatorService } from './shared'; export class UnixExecLocator extends AbstractLocatorService { - public get hasBinaries(): boolean { - return this.binary_paths.length > 0; + constructor() { + super(); + this._binaryPaths = []; + this._executables = []; } - public get binaries(): string[] { - return this.binary_paths; + public get hasExecutables(): boolean { + return this._executables.length > 0; + } + public get executables(): RExecutable[] { + return this._executables; + } + public get binaryPaths(): string[] { + return this._binaryPaths; } public refreshPaths(): void { - this.binary_paths = Array.from( + const paths = Array.from( new Set([ ...this.getHomeFromDirs(), ...this.getHomeFromEnv(), ... this.getHomeFromConda() ]) ); + for (const path of paths) { + if (!this._binaryPaths?.includes(path)) { + this._binaryPaths.push(path); + this._executables.push(RExecutableFactory.createExecutable(path)); + } + } } - private potential_bin_paths: string[] = [ - '/usr/lib64/R/bin/R', - '/usr/lib/R/bin/R', - '/usr/local/lib64/R/bin/R', - '/usr/local/lib/R/bin/R', - '/opt/local/lib64/R/bin/R', - '/opt/local/lib/R/bin/R' - ]; - private getHomeFromDirs(): string[] { const dirBins: string[] = []; - for (const bin of this.potential_bin_paths) { + const potentialPaths: string[] = [ + '/usr/lib64/R/bin/R', + '/usr/lib/R/bin/R', + '/usr/local/lib64/R/bin/R', + '/usr/local/lib/R/bin/R', + '/opt/local/lib64/R/bin/R', + '/opt/local/lib/R/bin/R' + ]; + + for (const bin of potentialPaths) { if (fs.existsSync(bin)) { dirBins.push(bin); } diff --git a/src/executables/locator/windows.ts b/src/executables/locator/windows.ts index 9c7a515f8..337da6b8b 100644 --- a/src/executables/locator/windows.ts +++ b/src/executables/locator/windows.ts @@ -1,19 +1,27 @@ import * as fs from 'fs-extra'; -import * as os from 'os'; import path = require('path'); import winreg = require('winreg'); +import { RExecutable, RExecutableFactory } from '../executable'; import { AbstractLocatorService } from './shared'; export class WindowsExecLocator extends AbstractLocatorService { - public get hasBinaries(): boolean { - return this.binary_paths.length > 0; + constructor() { + super(); + this._binaryPaths = []; + this._executables = []; } - public get binaries(): string[] { - return this.binary_paths; + public get hasExecutables(): boolean { + return this._executables.length > 0; + } + public get executables(): RExecutable[] { + return this._executables; + } + public get binaryPaths(): string[] { + return this._binaryPaths; } public refreshPaths(): void { - this.binary_paths = Array.from( + const paths = Array.from( new Set([ ...this.getHomeFromDirs(), ...this.getHomeFromEnv(), @@ -21,13 +29,14 @@ export class WindowsExecLocator extends AbstractLocatorService { // ... this.getHomeFromConda() ]) ); + for (const path of paths) { + if (!this._binaryPaths?.includes(path)) { + this._binaryPaths.push(path); + this._executables.push(RExecutableFactory.createExecutable(path)); + } + } } - private potential_bin_paths: string[] = [ - '%ProgramFiles%\\R\\', - '%ProgramFiles(x86)%\\R\\' - ]; - private getHomeFromRegistry(): string[] { const registryBins: string[] = []; const potentialBins = [ @@ -68,7 +77,11 @@ export class WindowsExecLocator extends AbstractLocatorService { private getHomeFromDirs(): string[] { const dirBins: string[] = []; - for (const bin of this.potential_bin_paths) { + const potential_bin_paths: string[] = [ + '%ProgramFiles%\\R\\', + '%ProgramFiles(x86)%\\R\\' + ]; + for (const bin of potential_bin_paths) { const resolvedBin = path.resolve(bin); if (fs.existsSync(resolvedBin)) { const i386 = `${resolvedBin}\\i386\\`; diff --git a/src/executables/ui/quickpick.ts b/src/executables/ui/quickpick.ts index 0a9ffd60c..c91a0cc25 100644 --- a/src/executables/ui/quickpick.ts +++ b/src/executables/ui/quickpick.ts @@ -84,10 +84,9 @@ export class ExecutableQuickPick implements vscode.Disposable { } ); - this.retriever.binaries.forEach(home => { + this.retriever.binaryPaths.forEach(home => { if (validateRFolder(home)) { - const inst = RExecutableFactory.createExecutable(home); - executables.push(inst); + executables.push(this.retriever.executables.filter(exec => exec.rBin === home)?.[0]); } }); sortBins(executables).forEach((bin: RExecutable) => { @@ -119,6 +118,7 @@ export class ExecutableQuickPick implements vscode.Disposable { function setupQuickpickListeners(self: ExecutableQuickPick, resolver: () => void): void { self.qp.onDidTriggerButton((item: vscode.QuickInputButton) => { if (item.tooltip === 'Refresh paths') { + self.retriever.refreshPaths(); self.setItems(); self.qp.show(); } else { From c8f7f6c9dd22ff48fd7ef40917b241f1bc635350 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Wed, 6 Jul 2022 12:16:24 +1000 Subject: [PATCH 03/22] Tooltip fixes Prevent outputting malformed tooltips when executable arch or version is undefined --- src/executables/executable.ts | 6 ++++-- src/executables/ui/status.ts | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/executables/executable.ts b/src/executables/executable.ts index cd294c09c..30a5a90a5 100644 --- a/src/executables/executable.ts +++ b/src/executables/executable.ts @@ -39,7 +39,9 @@ export class RExecutable { } public get tooltip(): string { - return `R ${this.rVersion} ${this.rArch}`; + const versionString = this.rVersion ? ` ${this.rVersion}` : ''; + const archString = this.rArch ? ` ${this.rArch}` : ''; + return `R${versionString}${archString}`; } } @@ -54,7 +56,7 @@ export class VirtualRExecutable extends RExecutable { } public get tooltip(): string { - return `${this.name} (R ${this.rVersion} ${this.rArch})`; + return `${this.name} (${super.tooltip})`; } // todo, hardcoded diff --git a/src/executables/ui/status.ts b/src/executables/ui/status.ts index 8ecbbe2f0..db70caeb9 100644 --- a/src/executables/ui/status.ts +++ b/src/executables/ui/status.ts @@ -34,7 +34,8 @@ export class ExecutableStatusItem implements vscode.Disposable { this.languageStatusItem.severity = vscode.LanguageStatusSeverity.Information; this.languageStatusItem.detail = execState.rBin; if (execState instanceof VirtualRExecutable) { - this.languageStatusItem.text = `${execState.name} (${execState.rVersion})`; + const versionString = execState.rVersion ? ` (${execState.rVersion})` : ''; + this.languageStatusItem.text = `${execState.name}${versionString}`; } else { this.languageStatusItem.text = execState.rVersion; } From fd1b174d9ba9f7ce0cd365899588913a92844931 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Sun, 10 Jul 2022 13:21:28 +1000 Subject: [PATCH 04/22] Refactor + simplify executable access Executables access is now handled purely through the RExecutableManager, rather than any member classes --- src/executables/index.ts | 71 +++---- src/executables/locator/shared.ts | 26 --- src/executables/service.ts | 144 -------------- .../{executable.ts => service/class.ts} | 38 +++- src/executables/service/index.ts | 175 ++++++++++++++++++ .../{ => service}/locator/index.ts | 2 +- src/executables/service/locator/shared.ts | 72 +++++++ src/executables/{ => service}/locator/unix.ts | 38 ++-- .../{ => service}/locator/windows.ts | 27 +-- src/executables/service/pathStorage.ts | 53 ++++++ src/executables/service/registry.ts | 27 +++ src/executables/storage.ts | 36 ---- src/executables/ui/index.ts | 4 +- src/executables/ui/quickpick.ts | 97 +++++----- src/executables/ui/status.ts | 6 +- 15 files changed, 466 insertions(+), 350 deletions(-) delete mode 100644 src/executables/locator/shared.ts delete mode 100644 src/executables/service.ts rename src/executables/{executable.ts => service/class.ts} (54%) create mode 100644 src/executables/service/index.ts rename src/executables/{ => service}/locator/index.ts (99%) create mode 100644 src/executables/service/locator/shared.ts rename src/executables/{ => service}/locator/unix.ts (78%) rename src/executables/{ => service}/locator/windows.ts (80%) create mode 100644 src/executables/service/pathStorage.ts create mode 100644 src/executables/service/registry.ts delete mode 100644 src/executables/storage.ts diff --git a/src/executables/index.ts b/src/executables/index.ts index de56eb1f2..52b77513e 100644 --- a/src/executables/index.ts +++ b/src/executables/index.ts @@ -2,26 +2,23 @@ import path = require('path'); import * as fs from 'fs-extra'; import * as vscode from 'vscode'; -import { LocatorServiceFactory, AbstractLocatorService } from './locator'; import { ExecutableStatusItem, ExecutableQuickPick } from './ui'; -import { RExecutableService, WorkspaceExecutableEvent } from './service'; +import { isVirtual, RExecutableService, ExecutableType, WorkspaceExecutableEvent } from './service'; import { extensionContext } from '../extension'; import { spawnAsync } from '../util'; -import { RExecutable, VirtualRExecutable } from './executable'; + +export { ExecutableType as IRExecutable, VirtualExecutableType as IVirtualRExecutable } from './service'; // super class that manages relevant sub classes -export class RExecutableManager { - private retrievalService: AbstractLocatorService; - private statusBar: ExecutableStatusItem; - private quickPick: ExecutableQuickPick; - private executableService: RExecutableService; +export class RExecutableManager implements vscode.Disposable { + private readonly statusBar: ExecutableStatusItem; + private readonly quickPick: ExecutableQuickPick; + private readonly executableService: RExecutableService; constructor() { - this.retrievalService = LocatorServiceFactory.getLocator(); - this.retrievalService.refreshPaths(); this.executableService = new RExecutableService(); this.statusBar = new ExecutableStatusItem(this.executableService); - this.quickPick = new ExecutableQuickPick(this.executableService, this.retrievalService); + this.quickPick = new ExecutableQuickPick(this.executableService); extensionContext.subscriptions.push( this.onDidChangeActiveExecutable(() => { @@ -32,26 +29,35 @@ export class RExecutableManager { this.reload(); } }), - this.executableService, - this.statusBar, - this.quickPick + this ); + this.reload(); } - public get activeExecutable(): RExecutable { - return this.executableService.activeExecutable; + public dispose(): void { + this.executableService.dispose(); + this.statusBar.dispose(); + this.quickPick.dispose(); } public get executableQuickPick(): ExecutableQuickPick { return this.quickPick; } - public get executableStatusItem(): ExecutableStatusItem { - return this.statusBar; + public get activeExecutablePath(): string { + return this.executableService.activeExecutable.rBin; + } + + public getExecutablePath(workingDir: string): string { + return this.executableService.getWorkspaceExecutable(workingDir).rBin; + } + + public get activeExecutable(): ExecutableType { + return this.executableService.activeExecutable; } - public get onDidChangeActiveExecutable(): vscode.Event { + public get onDidChangeActiveExecutable(): vscode.Event { return this.executableService.onDidChangeActiveExecutable; } @@ -72,21 +78,23 @@ export class RExecutableManager { } private async activateEnvironment(): Promise { + if (!isVirtual(this.activeExecutable) || + process.env.CONDA_DEFAULT_ENV !== this.activeExecutable.name) { + return Promise.resolve(); + } + const opts = { env: { ...process.env }, }; - if (this.activeExecutable instanceof VirtualRExecutable && opts.env.CONDA_DEFAULT_ENV !== this.activeExecutable.name) { - return spawnAsync( - 'conda', // hard coded for now - this.activeExecutable.activationCommand, - opts, - undefined - ); - } else { - return Promise.resolve(); - } + + return spawnAsync( + 'conda', // hard coded for now + this.activeExecutable.activationCommand, + opts, + undefined + ); } } @@ -99,8 +107,7 @@ export class RExecutableManager { * @param execPath * @returns boolean */ -export function validateRFolder(execPath: string): boolean { +export function validateRExecutablePath(execPath: string): boolean { const basename = process.platform === 'win32' ? 'R.exe' : 'R'; - const scriptPath = path.normalize(`${execPath}/../Rcmd`); - return fs.existsSync(execPath) && path.basename(basename) && fs.existsSync(scriptPath); + return fs.existsSync(execPath) && (path.basename(execPath) === basename); } diff --git a/src/executables/locator/shared.ts b/src/executables/locator/shared.ts deleted file mode 100644 index 55d25cfce..000000000 --- a/src/executables/locator/shared.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { execSync } from 'child_process'; -import { RExecutable } from '../executable'; - -export function getRDetailsFromPath(rPath: string): {version: string, arch: string} { - try { - const child = execSync(`${rPath} --version`).toString(); - const versionRegex = /(?<=R version\s)[0-9.]*/g; - const archRegex = /[0-9]*-bit/g; - const out = { - version: child.match(versionRegex)?.[0] ?? '', - arch: child.match(archRegex)?.[0] ?? '' - }; - return out; - } catch (error) { - return { version: '', arch: '' }; - } -} - -export abstract class AbstractLocatorService { - protected _binaryPaths: string[]; - protected _executables: RExecutable[]; - public abstract get hasExecutables(): boolean; - public abstract get executables(): RExecutable[]; - public abstract get binaryPaths(): string[]; - public abstract refreshPaths(): void; -} diff --git a/src/executables/service.ts b/src/executables/service.ts deleted file mode 100644 index bdb9ce937..000000000 --- a/src/executables/service.ts +++ /dev/null @@ -1,144 +0,0 @@ -import * as vscode from 'vscode'; - -import { validateRFolder } from '.'; -import { RExecutable, RExecutableFactory } from './executable'; -import { config, getCurrentWorkspaceFolder, getRPathConfigEntry } from '../util'; -import { clearExecutable, getExecutable, getExecutableStore, storeExecutable } from './storage'; - -/** - * @description - * @export - * @interface WorkspaceExecutableEvent - */ -export interface WorkspaceExecutableEvent { - workingFolder: vscode.WorkspaceFolder, - executable: RExecutable -} - -/** - * @description vf - * @export - * @class RExecutableService - * @implements {vscode.Disposable} - */ -export class RExecutableService implements vscode.Disposable { - private executableEmitter: vscode.EventEmitter; - private workspaceEmitter: vscode.EventEmitter; - private executables: Map; - - /** - * Creates an instance of RExecutableService. - * @memberof RExecutableService - */ - public constructor() { - this.executableEmitter = new vscode.EventEmitter(); - this.workspaceEmitter = new vscode.EventEmitter(); - this.executables = new Map(); - - const confPath = config().get(getRPathConfigEntry()); - for (const [dirPath, execPath] of getExecutableStore()) { - this.executables.set(dirPath, RExecutableFactory.createExecutable(execPath)); - } - - if (!getExecutable(getCurrentWorkspaceFolder().uri.fsPath) && confPath && validateRFolder(confPath)) { - console.log(`[RExecutableService] Executable set to configuration path: ${confPath}`); - const exec = RExecutableFactory.createExecutable(confPath); - this.activeExecutable = exec; - } - } - - - /** - * @description - * @memberof RExecutableService - */ - public set activeExecutable(executable: RExecutable) { - if (executable === null) { - this.executables.delete(getCurrentWorkspaceFolder().uri.fsPath); - clearExecutable(getCurrentWorkspaceFolder().uri.fsPath); - console.log('[RExecutableService] executable cleared'); - this.executableEmitter.fire(null); - } else if (this.activeExecutable !== executable) { - this.executables.set(getCurrentWorkspaceFolder().uri.fsPath, executable); - storeExecutable(executable.rBin, getCurrentWorkspaceFolder().uri.fsPath); - console.log('[RExecutableService] executable changed'); - this.executableEmitter.fire(executable); - } - } - - /** - * @description - * Returns the current *active* R executable. - * This may differ depending on the current active workspace folder. - * @type {RExecutable} - * @memberof RExecutableService - */ - public get activeExecutable(): RExecutable { - const currWorkspacePath = getCurrentWorkspaceFolder().uri.fsPath; - if (currWorkspacePath) { - return this.executables.get(currWorkspacePath); - } else { - return this.executables.get(vscode.window.activeTextEditor.document.uri.fsPath); - } - } - - /** - * @description - * Set the R executable associated with a given workspace folder. - * @param {string} folder - * @param {RExecutable} executable - * @memberof RExecutableService - */ - public setWorkspaceExecutable(folder: string, executable: RExecutable): void { - if (this.executables.get(folder) !== executable) { - const workspaceFolderUri = vscode.Uri.file(folder); - this.workspaceEmitter.fire({ workingFolder: vscode.workspace.getWorkspaceFolder(workspaceFolderUri), executable: executable }); - storeExecutable(executable.rBin, folder); - } - this.executables.set(folder, executable); - this.executableEmitter.fire(executable); - } - - /** - * @description - * Get the R executable associated with a given workspace folder. - * @param {string} folder - * @returns {*} {RExecutable} - * @memberof RExecutableService - */ - public getWorkspaceExecutable(folder: string): RExecutable { - return this.executables.get(folder); - } - - /** - * @description - * An event that is fired whenever the active executable changes. - * This can occur, for instance, when changing focus between multi-root workspaces. - * @readonly - * @type {vscode.Event} - * @memberof RExecutableService - */ - public get onDidChangeActiveExecutable(): vscode.Event { - return this.executableEmitter.event; - } - - /** - * @description - * Event that is triggered when the executable associated with a workspace is changed. - * @readonly - * @type {vscode.Event} - * @memberof RExecutableService - */ - public get onDidChangeWorkspaceExecutable(): vscode.Event { - return this.workspaceEmitter.event; - } - - /** - * @description - * @memberof RExecutableService - */ - public dispose(): void { - this.executableEmitter.dispose(); - this.workspaceEmitter.dispose(); - } -} diff --git a/src/executables/executable.ts b/src/executables/service/class.ts similarity index 54% rename from src/executables/executable.ts rename to src/executables/service/class.ts index 30a5a90a5..db4e23ca2 100644 --- a/src/executables/executable.ts +++ b/src/executables/service/class.ts @@ -1,20 +1,38 @@ import { getRDetailsFromPath } from './locator'; +import { RExecutableRegistry } from './registry'; + +export type ExecutableType = RExecutable; +export type VirtualExecutableType = VirtualRExecutable; + +export function isVirtual(executable: RExecutable): executable is VirtualRExecutable { + return executable instanceof VirtualRExecutable; +} export class RExecutableFactory { - static createExecutable(executablePath: string): RExecutable { - if (new RegExp('\\.conda').exec(executablePath)?.length > 0) { - return new VirtualRExecutable(executablePath); - } else { - return new RExecutable(executablePath); - } + private readonly registry: RExecutableRegistry; + + constructor(registry: RExecutableRegistry) { + this.registry = registry; } - private constructor() { - // + public create(executablePath: string): ExecutableType { + const oldExec = [...this.registry.executables.values()].find((v) => v.rBin === executablePath); + if (oldExec) { + return oldExec; + } else { + let executable: RExecutable; + if (new RegExp('\\.conda').exec(executablePath)?.length > 0) { + executable = new VirtualRExecutable(executablePath); + } else { + executable = new RExecutable(executablePath); + } + this.registry.addExecutable(executable); + return executable; + } } } -export class RExecutable { +class RExecutable { private _rBin: string; private _rVersion: string; private _arch: string; @@ -45,7 +63,7 @@ export class RExecutable { } } -export class VirtualRExecutable extends RExecutable { +class VirtualRExecutable extends RExecutable { constructor(bin_path: string) { super(bin_path); } diff --git a/src/executables/service/index.ts b/src/executables/service/index.ts new file mode 100644 index 000000000..76bcdf5a8 --- /dev/null +++ b/src/executables/service/index.ts @@ -0,0 +1,175 @@ +import * as vscode from 'vscode'; + +import { validateRExecutablePath } from '..'; +import { ExecutableType, RExecutableFactory } from './class'; +import { config, getCurrentWorkspaceFolder, getRPathConfigEntry } from '../../util'; +import { RExecutablePathStorage } from './pathStorage'; +import { RExecutableRegistry } from './registry'; +import { AbstractLocatorService, LocatorServiceFactory } from './locator'; + +export * from './class'; + +/** + * @description + * @export + * @interface WorkspaceExecutableEvent + */ +export interface WorkspaceExecutableEvent { + workingFolder: vscode.WorkspaceFolder, + executable: ExecutableType | undefined +} + +/** + * @description + * @export + * @class RExecutableService + * @implements {vscode.Disposable} + */ +export class RExecutableService implements vscode.Disposable { + public readonly executableFactory: RExecutableFactory; + public readonly executablePathLocator: AbstractLocatorService; + private readonly executableStorage: RExecutablePathStorage; + private readonly executableRegistry: RExecutableRegistry; + private executableEmitter: vscode.EventEmitter; + private workspaceEmitter: vscode.EventEmitter; + private workspaceExecutables: Map; + + + /** + * Creates an instance of RExecutableService. + * @memberof RExecutableService + */ + public constructor() { + this.executablePathLocator = LocatorServiceFactory.getLocator(); + this.executablePathLocator.refreshPaths(); + this.executableRegistry = new RExecutableRegistry(); + this.executableStorage = new RExecutablePathStorage(); + this.executableFactory = new RExecutableFactory(this.executableRegistry); + this.workspaceExecutables = new Map(); + + this.executableEmitter = new vscode.EventEmitter(); + this.workspaceEmitter = new vscode.EventEmitter(); + + // create executables for all executable paths found + this.executablePathLocator.binaryPaths.forEach((path) => { + this.executableFactory.create(path); + }); + + const confPath = config().get(getRPathConfigEntry()); + // from storage, recreate associations between workspace paths and executable paths + for (const [dirPath, execPath] of this.executableStorage.executablePaths) { + if (validateRExecutablePath(execPath)) { + this.workspaceExecutables.set(dirPath, this.executableFactory.create(execPath)); + } + } + + if (!this.executableStorage.getActiveExecutablePath() && confPath && validateRExecutablePath(confPath)) { + console.log(`[RExecutableService] Executable set to configuration path: ${confPath}`); + const exec = this.executableFactory.create(confPath); + this.activeExecutable = exec; + } + } + + public get executables(): Set { + return this.executableRegistry.executables; + } + + /** + * @description + * @memberof RExecutableService + */ + public set activeExecutable(executable: ExecutableType) { + if (executable === null) { + this.workspaceExecutables.delete(getCurrentWorkspaceFolder().uri.fsPath); + this.executableStorage.setExecutablePath(getCurrentWorkspaceFolder().uri.fsPath, null); + console.log('[RExecutableService] executable cleared'); + this.executableEmitter.fire(null); + } else if (this.activeExecutable !== executable) { + this.workspaceExecutables.set(getCurrentWorkspaceFolder().uri.fsPath, executable); + this.executableStorage.setExecutablePath(getCurrentWorkspaceFolder().uri.fsPath, executable.rBin); + console.log('[RExecutableService] executable changed'); + this.executableEmitter.fire(executable); + } + } + + /** + * @description + * Returns the current *active* R executable. + * This may differ depending on the current active workspace folder. + * @type {RExecutable} + * @memberof RExecutableService + */ + public get activeExecutable(): ExecutableType | undefined { + const currWorkspacePath = getCurrentWorkspaceFolder().uri.fsPath; + if (currWorkspacePath) { + return this.workspaceExecutables.get(currWorkspacePath); + } else { + return this.workspaceExecutables.get(vscode.window.activeTextEditor.document.uri.fsPath); + } + } + + /** + * @description + * Set the R executable associated with a given workspace folder. + * @param {string} folder + * @param {RExecutable} executable + * @memberof RExecutableService + */ + public setWorkspaceExecutable(folder: string, executable: ExecutableType): void { + if (this.workspaceExecutables.get(folder) !== executable) { + if (executable === undefined) { + this.executableStorage.setExecutablePath(folder, undefined); + this.workspaceEmitter.fire({ workingFolder: undefined, executable: executable }); + } else { + const workspaceFolderUri = vscode.Uri.file(folder); + this.workspaceEmitter.fire({ workingFolder: vscode.workspace.getWorkspaceFolder(workspaceFolderUri), executable: executable }); + this.executableStorage.setExecutablePath(folder, executable.rBin); + } + } + this.workspaceExecutables.set(folder, executable); + this.executableEmitter.fire(executable); + } + + /** + * @description + * Get the R executable associated with a given workspace folder. + * @param {string} folder + * @returns {*} {RExecutable} + * @memberof RExecutableService + */ + public getWorkspaceExecutable(folder: string): ExecutableType | undefined{ + return this.workspaceExecutables.get(folder); + } + + /** + * @description + * An event that is fired whenever the active executable changes. + * This can occur, for instance, when changing focus between multi-root workspaces. + * @readonly + * @type {vscode.Event} + * @memberof RExecutableService + */ + public get onDidChangeActiveExecutable(): vscode.Event { + return this.executableEmitter.event; + } + + /** + * @description + * Event that is triggered when the executable associated with a workspace is changed. + * @readonly + * @type {vscode.Event} + * @memberof RExecutableService + */ + public get onDidChangeWorkspaceExecutable(): vscode.Event { + return this.workspaceEmitter.event; + } + + /** + * @description + * @memberof RExecutableService + */ + public dispose(): void { + this.executableEmitter.dispose(); + this.workspaceEmitter.dispose(); + } +} diff --git a/src/executables/locator/index.ts b/src/executables/service/locator/index.ts similarity index 99% rename from src/executables/locator/index.ts rename to src/executables/service/locator/index.ts index ccdd6339a..b7925dec1 100644 --- a/src/executables/locator/index.ts +++ b/src/executables/service/locator/index.ts @@ -16,4 +16,4 @@ export class LocatorServiceFactory { private constructor() { // } -} \ No newline at end of file +} diff --git a/src/executables/service/locator/shared.ts b/src/executables/service/locator/shared.ts new file mode 100644 index 000000000..cdf941f36 --- /dev/null +++ b/src/executables/service/locator/shared.ts @@ -0,0 +1,72 @@ +import { execSync } from 'child_process'; +import * as fs from 'fs-extra'; +import * as vscode from 'vscode'; + +export function getRDetailsFromPath(rPath: string): {version: string, arch: string} { + try { + const child = execSync(`${rPath} --version`).toString(); + const versionRegex = /(?<=R version\s)[0-9.]*/g; + const archRegex = /[0-9]*-bit/g; + const out = { + version: child.match(versionRegex)?.[0] ?? '', + arch: child.match(archRegex)?.[0] ?? '' + }; + return out; + } catch (error) { + return { version: '', arch: '' }; + } +} + +/** + * For a given array of paths, return only unique paths + * (including symlinks), favouring shorter paths + * @param paths + */ +export function getUniquePaths(paths: string[]): string[] { + function realpath(path: string): string { + if (fs.lstatSync(path).isSymbolicLink()) { + return fs.realpathSync(path); + } + return path; + } + function existsInSet(set: Set, path: string): string { + const arr: string[] = []; + set.forEach((v) => { + if (realpath(path) === realpath(v)) { + arr.push(v); + } + }); + return arr?.[0]; + } + + const out: Set = new Set(); + for (const path of paths) { + const truepath = realpath(path); + const storedpath = existsInSet(out, path); + if (storedpath) { + if (storedpath.length > truepath.length) { + out.delete(storedpath); + out.add(truepath); + } + } else { + const shortestPath = truepath.length <= path.length ? truepath : path; + out.add(shortestPath); + } + } + return [...out.values()]; +} + +export abstract class AbstractLocatorService { + protected _binaryPaths: string[]; + protected emitter: vscode.EventEmitter; + public abstract refreshPaths(): void; + public get hasPaths(): boolean { + return this._binaryPaths.length > 0; + } + public get binaryPaths(): string[] { + return this._binaryPaths; + } + public get onDidRefreshPaths(): vscode.Event { + return this.emitter.event; + } +} \ No newline at end of file diff --git a/src/executables/locator/unix.ts b/src/executables/service/locator/unix.ts similarity index 78% rename from src/executables/locator/unix.ts rename to src/executables/service/locator/unix.ts index 358a0f5c7..07151c581 100644 --- a/src/executables/locator/unix.ts +++ b/src/executables/service/locator/unix.ts @@ -1,39 +1,25 @@ import * as fs from 'fs-extra'; import * as os from 'os'; +import * as vscode from 'vscode'; import path = require('path'); -import { RExecutable, RExecutableFactory } from '../executable'; - +import { getUniquePaths } from './shared'; import { AbstractLocatorService } from './shared'; export class UnixExecLocator extends AbstractLocatorService { constructor() { super(); + this.emitter = new vscode.EventEmitter(); this._binaryPaths = []; - this._executables = []; - } - public get hasExecutables(): boolean { - return this._executables.length > 0; - } - public get executables(): RExecutable[] { - return this._executables; - } - public get binaryPaths(): string[] { - return this._binaryPaths; } public refreshPaths(): void { - const paths = Array.from( + this._binaryPaths = getUniquePaths(Array.from( new Set([ ...this.getHomeFromDirs(), ...this.getHomeFromEnv(), ... this.getHomeFromConda() ]) - ); - for (const path of paths) { - if (!this._binaryPaths?.includes(path)) { - this._binaryPaths.push(path); - this._executables.push(RExecutableFactory.createExecutable(path)); - } - } + )); + this.emitter.fire(this._binaryPaths); } private getHomeFromDirs(): string[] { @@ -93,4 +79,16 @@ export class UnixExecLocator extends AbstractLocatorService { } return envBins; } + + // private getHomeFromStorage(): string[] { + // const store = getExecutableStore(); + // const storedBins: string[] = []; + // for (const [_, path] of store) { + // if (fs.existsSync(path) && validateRExecutablePath(path)) { + // storedBins.push(path); + // } + // } + // return storedBins; + // } } + diff --git a/src/executables/locator/windows.ts b/src/executables/service/locator/windows.ts similarity index 80% rename from src/executables/locator/windows.ts rename to src/executables/service/locator/windows.ts index 337da6b8b..b5239e2b6 100644 --- a/src/executables/locator/windows.ts +++ b/src/executables/service/locator/windows.ts @@ -1,40 +1,25 @@ import * as fs from 'fs-extra'; +import * as vscode from 'vscode'; import path = require('path'); import winreg = require('winreg'); -import { RExecutable, RExecutableFactory } from '../executable'; - -import { AbstractLocatorService } from './shared'; +import { getUniquePaths, AbstractLocatorService } from './shared'; export class WindowsExecLocator extends AbstractLocatorService { constructor() { super(); + this.emitter = new vscode.EventEmitter(); this._binaryPaths = []; - this._executables = []; - } - public get hasExecutables(): boolean { - return this._executables.length > 0; - } - public get executables(): RExecutable[] { - return this._executables; - } - public get binaryPaths(): string[] { - return this._binaryPaths; } public refreshPaths(): void { - const paths = Array.from( + this._binaryPaths = getUniquePaths(Array.from( new Set([ ...this.getHomeFromDirs(), ...this.getHomeFromEnv(), ...this.getHomeFromRegistry(), // ... this.getHomeFromConda() ]) - ); - for (const path of paths) { - if (!this._binaryPaths?.includes(path)) { - this._binaryPaths.push(path); - this._executables.push(RExecutableFactory.createExecutable(path)); - } - } + )); + this.emitter.fire(this._binaryPaths); } private getHomeFromRegistry(): string[] { diff --git a/src/executables/service/pathStorage.ts b/src/executables/service/pathStorage.ts new file mode 100644 index 000000000..074d623fb --- /dev/null +++ b/src/executables/service/pathStorage.ts @@ -0,0 +1,53 @@ +import { extensionContext } from '../../extension'; +import { getCurrentWorkspaceFolder } from '../../util'; + + +export class RExecutablePathStorage { + private store: Map; + + constructor() { + this.store = this.getExecutableStore(); + } + + public get executablePaths(): Map { + return this.store; + } + + public setExecutablePath(workingDir: string, binPath: string): void { + if (binPath) { + this.store.set(workingDir, binPath); + } else { + this.store.delete(workingDir); + } + void this.saveStorage(); + } + + public getActiveExecutablePath(): string { + return this.store.get(getCurrentWorkspaceFolder().uri.fsPath); + } + + public getExecutablePath(workingDir: string): string { + return this.store.get(workingDir); + } + + private getExecutableStore(): Map { + return this.stringToMap(extensionContext.globalState.get('rExecMap', '')); + } + + private async saveStorage(): Promise { + const out = this.mapToString(this.store); + await extensionContext.globalState.update('rExecMap', out); + } + + private mapToString(map: Map): string { + return JSON.stringify([...map]); + } + + private stringToMap(str: string): Map { + try { + return new Map(JSON.parse(str)); + } catch (error) { + return new Map(); + } + } +} \ No newline at end of file diff --git a/src/executables/service/registry.ts b/src/executables/service/registry.ts new file mode 100644 index 000000000..6d5a4d2c7 --- /dev/null +++ b/src/executables/service/registry.ts @@ -0,0 +1,27 @@ +import { ExecutableType } from './class'; + +// necessary to have an executable registry +// so that we don't spam the (re)creation of executables +export class RExecutableRegistry { + private readonly _executables: Set; + + constructor() { + this._executables = new Set(); + } + + public get executables(): Set { + return this._executables; + } + + public addExecutable(executable: ExecutableType): Set { + return this._executables.add(executable); + } + + public deleteExecutable(executable: ExecutableType): boolean { + return this._executables.delete(executable); + } + + public hasExecutable(executable: ExecutableType): boolean { + return this._executables.has(executable); + } +} diff --git a/src/executables/storage.ts b/src/executables/storage.ts deleted file mode 100644 index bab8fd23c..000000000 --- a/src/executables/storage.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { extensionContext } from '../extension'; - -/** - * Executables are stored as a map of working directories and bin paths - */ -export function getExecutableStore(): Map { - return stringToMap(extensionContext.globalState.get('rExecMap', '')); -} - -export function getExecutable(workingDir: string): string { - const store: Map = getExecutableStore(); - return store.get(workingDir); -} - -export function storeExecutable(binPath: string, workingDir: string): void { - const currentStore: Map = getExecutableStore(); - currentStore.set(workingDir, binPath); - void extensionContext.globalState.update('rExecMap', mapToString(currentStore)); -} - -export function clearExecutable(workingDir: string): boolean { - const currentStore: Map = getExecutableStore(); - return currentStore.delete(workingDir); -} - -function mapToString(map: Map): string { - return JSON.stringify([...map]); -} - -function stringToMap(str: string): Map { - try { - return new Map(JSON.parse(str)); - } catch (error) { - return new Map(); - } -} \ No newline at end of file diff --git a/src/executables/ui/index.ts b/src/executables/ui/index.ts index 91e4545af..6d709e4bc 100644 --- a/src/executables/ui/index.ts +++ b/src/executables/ui/index.ts @@ -2,6 +2,6 @@ export { ExecutableQuickPick } from './quickpick'; export { ExecutableStatusItem } from './status'; export enum ExecutableNotifications { - badFolder = 'Supplied R executable directory is not a valid R directory.', - badConfig = 'Configured path is not a valid R executable directory.' + badFolder = 'Supplied R executable path is not a valid R path.', + badConfig = 'Configured path is not a valid R executable path.' } \ No newline at end of file diff --git a/src/executables/ui/quickpick.ts b/src/executables/ui/quickpick.ts index c91a0cc25..7337d7769 100644 --- a/src/executables/ui/quickpick.ts +++ b/src/executables/ui/quickpick.ts @@ -1,33 +1,26 @@ import * as vscode from 'vscode'; import { ExecutableNotifications } from '.'; -import { validateRFolder } from '..'; +import { validateRExecutablePath } from '..'; import { config, getCurrentWorkspaceFolder, getRPathConfigEntry } from '../../util'; -import { RExecutable, RExecutableFactory, VirtualRExecutable } from '../executable'; -import { AbstractLocatorService } from '../locator'; +import { isVirtual, ExecutableType } from '../service'; import { RExecutableService } from '../service'; - -const confRpath = () => { - return config().get(getRPathConfigEntry()); -}; - class ExecutableQuickPickItem implements vscode.QuickPickItem { public label: string; public description: string; public detail?: string; public picked?: boolean; public alwaysShow?: boolean; + private _executable: ExecutableType; - private _executable: RExecutable = undefined; - - constructor(executable: RExecutable) { + constructor(executable: ExecutableType) { this._executable = executable; this.label = executable.tooltip; this.description = executable.rBin; } - public get executable(): RExecutable { + public get executable(): ExecutableType { return this._executable; } @@ -39,27 +32,25 @@ enum PathQuickPickMenu { } export class ExecutableQuickPick implements vscode.Disposable { - private qp: vscode.QuickPick; - private retriever: AbstractLocatorService; - private service: RExecutableService; + private readonly service: RExecutableService; + private quickpick: vscode.QuickPick; private currentFolder: string; - public constructor(service: RExecutableService, retriever: AbstractLocatorService) { + public constructor(service: RExecutableService) { this.service = service; - this.retriever = retriever; this.currentFolder = getCurrentWorkspaceFolder().uri.fsPath; } public dispose(): void { - this.qp.dispose(); + this.quickpick.dispose(); } private setItems(): void { - function sortBins(bins: RExecutable[]) { + function sortBins(bins: ExecutableType[]) { return bins.sort((a, b) => { - if (a instanceof RExecutable && b instanceof VirtualRExecutable) { + if (!isVirtual(a) && isVirtual(b)) { return 1; - } else if (b instanceof RExecutable && a instanceof VirtualRExecutable) { + } else if (!isVirtual(b) && isVirtual(a)) { return -1; } else { return a.rVersion.localeCompare(b.rVersion, undefined, { numeric: true, sensitivity: 'base' }); @@ -67,8 +58,7 @@ export class ExecutableQuickPick implements vscode.Disposable { }); } const qpItems: vscode.QuickPickItem[] = []; - const executables: RExecutable[] = []; - const configPath = confRpath(); + const configPath = config().get(getRPathConfigEntry()); qpItems.push( { label: PathQuickPickMenu.search, @@ -79,54 +69,50 @@ export class ExecutableQuickPick implements vscode.Disposable { label: PathQuickPickMenu.configuration, alwaysShow: true, description: configPath, - detail: validateRFolder(configPath) ? '' : 'Invalid R folder', + detail: validateRExecutablePath(configPath) ? '' : 'Invalid R folder', picked: false } ); - this.retriever.binaryPaths.forEach(home => { - if (validateRFolder(home)) { - executables.push(this.retriever.executables.filter(exec => exec.rBin === home)?.[0]); - } - }); - sortBins(executables).forEach((bin: RExecutable) => { + sortBins([...this.service.executables]).forEach((bin: ExecutableType) => { qpItems.push(new ExecutableQuickPickItem(bin)); }); - this.qp.items = qpItems; - for (const item of this.qp.items) { + this.quickpick.items = qpItems; + + for (const item of this.quickpick.items) { if (item.description === this.service.getWorkspaceExecutable(this.currentFolder)?.rBin) { - this.qp.activeItems = [item]; + this.quickpick.activeItems = [item]; } } } public async showQuickPick(): Promise { function setupQuickpickOpts(self: ExecutableQuickPick): void { - self.qp = vscode.window.createQuickPick(); - self.qp.title = 'Select R executable path'; - self.qp.canSelectMany = false; - self.qp.ignoreFocusOut = true; - self.qp.matchOnDescription = true; - self.qp.placeholder = ''; - self.qp.buttons = [ + self.quickpick = vscode.window.createQuickPick(); + self.quickpick.title = 'Select R executable path'; + self.quickpick.canSelectMany = false; + self.quickpick.ignoreFocusOut = true; + self.quickpick.matchOnDescription = true; + self.quickpick.placeholder = ''; + self.quickpick.buttons = [ { iconPath: new vscode.ThemeIcon('clear-all'), tooltip: 'Clear stored path' }, { iconPath: new vscode.ThemeIcon('refresh'), tooltip: 'Refresh paths' } ]; } function setupQuickpickListeners(self: ExecutableQuickPick, resolver: () => void): void { - self.qp.onDidTriggerButton((item: vscode.QuickInputButton) => { + self.quickpick.onDidTriggerButton((item: vscode.QuickInputButton) => { if (item.tooltip === 'Refresh paths') { - self.retriever.refreshPaths(); + self.service.executablePathLocator.refreshPaths(); self.setItems(); - self.qp.show(); + self.quickpick.show(); } else { - self.service.setWorkspaceExecutable(self.currentFolder, null); - self.qp.hide(); + self.service.setWorkspaceExecutable(self.currentFolder, undefined); + self.quickpick.hide(); } }); - self.qp.onDidChangeSelection((items: vscode.QuickPickItem[]) => { + self.quickpick.onDidChangeSelection((items: vscode.QuickPickItem[]) => { const qpItem = items[0]; if (qpItem.label) { switch (qpItem.label) { @@ -137,24 +123,25 @@ export class ExecutableQuickPick implements vscode.Disposable { canSelectMany: false, title: ' R executable file' }; - void vscode.window.showOpenDialog(opts).then((exec_path) => { - if (validateRFolder(exec_path[0].fsPath)) { - const rExec = RExecutableFactory.createExecutable(exec_path[0].fsPath); + void vscode.window.showOpenDialog(opts).then((execPath) => { + if (validateRExecutablePath(execPath[0].fsPath)) { + const rExec = self.service.executableFactory.create(execPath[0].fsPath); self.service.setWorkspaceExecutable(self.currentFolder, rExec); } else { void vscode.window.showErrorMessage(ExecutableNotifications.badFolder); - self.service.setWorkspaceExecutable(self.currentFolder, null); + self.service.setWorkspaceExecutable(self.currentFolder, undefined); } }); break; } case PathQuickPickMenu.configuration: { - if (validateRFolder(confRpath())) { - const rExec = RExecutableFactory.createExecutable(confRpath()); + const configPath = config().get(getRPathConfigEntry()); + if (validateRExecutablePath(configPath)) { + const rExec = self.service.executableFactory.create(configPath); self.service.setWorkspaceExecutable(self.currentFolder, rExec); } else { void vscode.window.showErrorMessage(ExecutableNotifications.badConfig); - self.service.setWorkspaceExecutable(self.currentFolder, null); + self.service.setWorkspaceExecutable(self.currentFolder, undefined); } break; } @@ -164,7 +151,7 @@ export class ExecutableQuickPick implements vscode.Disposable { } } } - self.qp.hide(); + self.quickpick.hide(); resolver(); }); } @@ -175,7 +162,7 @@ export class ExecutableQuickPick implements vscode.Disposable { void showWorkspaceFolderQP().then((folder: vscode.WorkspaceFolder) => { this.currentFolder = folder.uri.fsPath; this.setItems(); - this.qp.show(); + this.quickpick.show(); }); }); } diff --git a/src/executables/ui/status.ts b/src/executables/ui/status.ts index db70caeb9..40fe49872 100644 --- a/src/executables/ui/status.ts +++ b/src/executables/ui/status.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; -import { VirtualRExecutable } from '../executable'; +import { isVirtual } from '../service/class'; import { RExecutableService } from '../service'; enum BinText { @@ -9,7 +9,7 @@ enum BinText { } export class ExecutableStatusItem implements vscode.Disposable { - private service: RExecutableService; + private readonly service: RExecutableService; private languageStatusItem: vscode.LanguageStatusItem; private createItem(): vscode.LanguageStatusItem { @@ -33,7 +33,7 @@ export class ExecutableStatusItem implements vscode.Disposable { if (execState) { this.languageStatusItem.severity = vscode.LanguageStatusSeverity.Information; this.languageStatusItem.detail = execState.rBin; - if (execState instanceof VirtualRExecutable) { + if (isVirtual(execState)) { const versionString = execState.rVersion ? ` (${execState.rVersion})` : ''; this.languageStatusItem.text = `${execState.name}${versionString}`; } else { From 82b08de2766241c4ce7c89fcef24232bcaabc6ec Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Mon, 11 Jul 2022 15:25:44 +1000 Subject: [PATCH 05/22] Binary recommendations, fix undefined, revert LSP changes - Revert language service changes - need to look at how python does it - Recommend binaries that are in the workspace or are specified by a renv lockfile - Fix possibly undefined issues - Normalise path for windows - Update vscode types and engine to 1.65 - Async init method for the manager --- package.json | 4 +- src/executables/index.ts | 30 ++--- src/executables/service/class.ts | 15 ++- src/executables/service/index.ts | 123 +++++++++++++++------ src/executables/service/locator/index.ts | 2 + src/executables/service/locator/shared.ts | 8 +- src/executables/service/locator/unix.ts | 16 ++- src/executables/service/locator/windows.ts | 21 ++-- src/executables/service/pathStorage.ts | 6 +- src/executables/service/registry.ts | 4 + src/executables/service/renv.ts | 22 ++++ src/executables/ui/quickpick.ts | 90 ++++++++++----- src/executables/ui/status.ts | 5 +- src/extension.ts | 2 +- src/util.ts | 22 ++-- yarn.lock | 8 +- 16 files changed, 262 insertions(+), 116 deletions(-) create mode 100644 src/executables/service/renv.ts diff --git a/package.json b/package.json index 5915912e7..07de3b608 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "R Markdown" ], "engines": { - "vscode": "^1.60.0" + "vscode": "^1.65.0" }, "activationEvents": [ "onLanguage:r", @@ -1916,7 +1916,7 @@ "@types/node": "^14.17.3", "@types/node-fetch": "^2.5.10", "@types/showdown": "^1.9.3", - "@types/vscode": "^1.60.0", + "@types/vscode": "^1.65.0", "@types/winreg": "^1.2.31", "@types/ws": "^7.4.4", "@typescript-eslint/eslint-plugin": "4.25.0", diff --git a/src/executables/index.ts b/src/executables/index.ts index 52b77513e..9240cbbbd 100644 --- a/src/executables/index.ts +++ b/src/executables/index.ts @@ -11,15 +11,14 @@ export { ExecutableType as IRExecutable, VirtualExecutableType as IVirtualRExecu // super class that manages relevant sub classes export class RExecutableManager implements vscode.Disposable { - private readonly statusBar: ExecutableStatusItem; - private readonly quickPick: ExecutableQuickPick; private readonly executableService: RExecutableService; + private statusBar: ExecutableStatusItem; + private quickPick: ExecutableQuickPick; - constructor() { - this.executableService = new RExecutableService(); + private constructor(service: RExecutableService) { + this.executableService = service; this.statusBar = new ExecutableStatusItem(this.executableService); this.quickPick = new ExecutableQuickPick(this.executableService); - extensionContext.subscriptions.push( this.onDidChangeActiveExecutable(() => { this.reload(); @@ -31,10 +30,14 @@ export class RExecutableManager implements vscode.Disposable { }), this ); - this.reload(); } + static async initialize(): Promise { + const executableService = await RExecutableService.initialize(); + return new this(executableService); + } + public dispose(): void { this.executableService.dispose(); this.statusBar.dispose(); @@ -45,19 +48,19 @@ export class RExecutableManager implements vscode.Disposable { return this.quickPick; } - public get activeExecutablePath(): string { - return this.executableService.activeExecutable.rBin; + public get activeExecutablePath(): string | undefined { + return this.executableService.activeExecutable?.rBin; } - public getExecutablePath(workingDir: string): string { - return this.executableService.getWorkspaceExecutable(workingDir).rBin; + public getExecutablePath(workingDir: string): string | undefined { + return this.executableService.getWorkspaceExecutable(workingDir)?.rBin; } - public get activeExecutable(): ExecutableType { + public get activeExecutable(): ExecutableType | undefined { return this.executableService.activeExecutable; } - public get onDidChangeActiveExecutable(): vscode.Event { + public get onDidChangeActiveExecutable(): vscode.Event { return this.executableService.onDidChangeActiveExecutable; } @@ -77,8 +80,9 @@ export class RExecutableManager implements vscode.Disposable { void this.statusBar.busy(loading); } + private async activateEnvironment(): Promise { - if (!isVirtual(this.activeExecutable) || + if (!this.activeExecutable || !isVirtual(this.activeExecutable) || process.env.CONDA_DEFAULT_ENV !== this.activeExecutable.name) { return Promise.resolve(); } diff --git a/src/executables/service/class.ts b/src/executables/service/class.ts index db4e23ca2..e1f07fe13 100644 --- a/src/executables/service/class.ts +++ b/src/executables/service/class.ts @@ -21,7 +21,7 @@ export class RExecutableFactory { return oldExec; } else { let executable: RExecutable; - if (new RegExp('\\.conda').exec(executablePath)?.length > 0) { + if (new RegExp('\\.conda')?.exec(executablePath)) { executable = new VirtualRExecutable(executablePath); } else { executable = new RExecutable(executablePath); @@ -64,13 +64,16 @@ class RExecutable { } class VirtualRExecutable extends RExecutable { + private _name: string; + constructor(bin_path: string) { super(bin_path); + const reg = new RegExp('(?<=\\/envs\\/)(.*?)(?=\\/)'); + this._name = reg?.exec(this.rBin)?.[0] ?? ''; } public get name(): string { - const reg = new RegExp('(?<=\\/envs\\/)(.*?)(?=\\/)'); - return reg.exec(this.rBin)[0]; + return this._name; } public get tooltip(): string { @@ -79,6 +82,10 @@ class VirtualRExecutable extends RExecutable { // todo, hardcoded public get activationCommand(): string[] { - return ['activate', this.name]; + if (this.name) { + return ['activate', this.name]; + } else { + return ['activate']; + } } } diff --git a/src/executables/service/index.ts b/src/executables/service/index.ts index 76bcdf5a8..d127c2915 100644 --- a/src/executables/service/index.ts +++ b/src/executables/service/index.ts @@ -6,6 +6,7 @@ import { config, getCurrentWorkspaceFolder, getRPathConfigEntry } from '../../ut import { RExecutablePathStorage } from './pathStorage'; import { RExecutableRegistry } from './registry'; import { AbstractLocatorService, LocatorServiceFactory } from './locator'; +import { getRenvVersion } from './renv'; export * from './class'; @@ -15,7 +16,7 @@ export * from './class'; * @interface WorkspaceExecutableEvent */ export interface WorkspaceExecutableEvent { - workingFolder: vscode.WorkspaceFolder, + workingFolder: vscode.WorkspaceFolder | undefined, executable: ExecutableType | undefined } @@ -26,50 +27,47 @@ export interface WorkspaceExecutableEvent { * @implements {vscode.Disposable} */ export class RExecutableService implements vscode.Disposable { - public readonly executableFactory: RExecutableFactory; - public readonly executablePathLocator: AbstractLocatorService; - private readonly executableStorage: RExecutablePathStorage; - private readonly executableRegistry: RExecutableRegistry; - private executableEmitter: vscode.EventEmitter; + public executableFactory: RExecutableFactory; + public executablePathLocator: AbstractLocatorService; + private executableStorage: RExecutablePathStorage; + private executableRegistry: RExecutableRegistry; + private executableEmitter: vscode.EventEmitter; private workspaceEmitter: vscode.EventEmitter; - private workspaceExecutables: Map; + private workspaceExecutables: Map; + public readonly ready: Thenable; /** * Creates an instance of RExecutableService. * @memberof RExecutableService */ - public constructor() { - this.executablePathLocator = LocatorServiceFactory.getLocator(); - this.executablePathLocator.refreshPaths(); + private constructor(locator: AbstractLocatorService) { + this.executablePathLocator = locator; this.executableRegistry = new RExecutableRegistry(); this.executableStorage = new RExecutablePathStorage(); this.executableFactory = new RExecutableFactory(this.executableRegistry); this.workspaceExecutables = new Map(); - this.executableEmitter = new vscode.EventEmitter(); this.workspaceEmitter = new vscode.EventEmitter(); - - // create executables for all executable paths found this.executablePathLocator.binaryPaths.forEach((path) => { this.executableFactory.create(path); }); - const confPath = config().get(getRPathConfigEntry()); - // from storage, recreate associations between workspace paths and executable paths - for (const [dirPath, execPath] of this.executableStorage.executablePaths) { - if (validateRExecutablePath(execPath)) { - this.workspaceExecutables.set(dirPath, this.executableFactory.create(execPath)); - } - } + this.selectViableExecutables(); + } - if (!this.executableStorage.getActiveExecutablePath() && confPath && validateRExecutablePath(confPath)) { - console.log(`[RExecutableService] Executable set to configuration path: ${confPath}`); - const exec = this.executableFactory.create(confPath); - this.activeExecutable = exec; - } + static async initialize(): Promise { + const locator = LocatorServiceFactory.getLocator(); + await locator.refreshPaths(); + return new this(locator); } + /** + * @description + * @readonly + * @type {Set} + * @memberof RExecutableService + */ public get executables(): Set { return this.executableRegistry.executables; } @@ -78,12 +76,12 @@ export class RExecutableService implements vscode.Disposable { * @description * @memberof RExecutableService */ - public set activeExecutable(executable: ExecutableType) { - if (executable === null) { + public set activeExecutable(executable: ExecutableType | undefined) { + if (executable === undefined) { this.workspaceExecutables.delete(getCurrentWorkspaceFolder().uri.fsPath); - this.executableStorage.setExecutablePath(getCurrentWorkspaceFolder().uri.fsPath, null); + this.executableStorage.setExecutablePath(getCurrentWorkspaceFolder().uri.fsPath, undefined); console.log('[RExecutableService] executable cleared'); - this.executableEmitter.fire(null); + this.executableEmitter.fire(undefined); } else if (this.activeExecutable !== executable) { this.workspaceExecutables.set(getCurrentWorkspaceFolder().uri.fsPath, executable); this.executableStorage.setExecutablePath(getCurrentWorkspaceFolder().uri.fsPath, executable.rBin); @@ -100,12 +98,17 @@ export class RExecutableService implements vscode.Disposable { * @memberof RExecutableService */ public get activeExecutable(): ExecutableType | undefined { - const currWorkspacePath = getCurrentWorkspaceFolder().uri.fsPath; + const currWorkspacePath = getCurrentWorkspaceFolder()?.uri?.fsPath; if (currWorkspacePath) { return this.workspaceExecutables.get(currWorkspacePath); - } else { - return this.workspaceExecutables.get(vscode.window.activeTextEditor.document.uri.fsPath); } + + const currentDocument = vscode?.window?.activeTextEditor?.document?.uri?.fsPath; + if (currentDocument) { + return this.workspaceExecutables.get(currentDocument); + } + + return undefined; } /** @@ -115,7 +118,7 @@ export class RExecutableService implements vscode.Disposable { * @param {RExecutable} executable * @memberof RExecutableService */ - public setWorkspaceExecutable(folder: string, executable: ExecutableType): void { + public setWorkspaceExecutable(folder: string, executable: ExecutableType | undefined): void { if (this.workspaceExecutables.get(folder) !== executable) { if (executable === undefined) { this.executableStorage.setExecutablePath(folder, undefined); @@ -137,7 +140,7 @@ export class RExecutableService implements vscode.Disposable { * @returns {*} {RExecutable} * @memberof RExecutableService */ - public getWorkspaceExecutable(folder: string): ExecutableType | undefined{ + public getWorkspaceExecutable(folder: string): ExecutableType | undefined { return this.workspaceExecutables.get(folder); } @@ -149,7 +152,7 @@ export class RExecutableService implements vscode.Disposable { * @type {vscode.Event} * @memberof RExecutableService */ - public get onDidChangeActiveExecutable(): vscode.Event { + public get onDidChangeActiveExecutable(): vscode.Event { return this.executableEmitter.event; } @@ -172,4 +175,54 @@ export class RExecutableService implements vscode.Disposable { this.executableEmitter.dispose(); this.workspaceEmitter.dispose(); } + + private selectViableExecutables(): void { + // from storage, recreate associations between workspace paths and executable paths + for (const [dirPath, execPath] of this.executableStorage.executablePaths) { + if (validateRExecutablePath(execPath)) { + this.workspaceExecutables.set(dirPath, this.executableFactory.create(execPath)); + } + } + + const confPath = config().get(getRPathConfigEntry()); + if (vscode.workspace.workspaceFolders) { + for (const workspace of vscode.workspace.workspaceFolders) { + const workspacePath = workspace.uri.path; + if (!this.workspaceExecutables.has(workspacePath)) { + // is there a local virtual env? + // todo + + // is there a renv-recommended version? + const renvVersion = getRenvVersion(workspacePath); + if (renvVersion) { + const compatibleExecutables = this.executableRegistry.getExecutablesWithVersion(renvVersion); + if (compatibleExecutables) { + const exec = compatibleExecutables.sort((a, b) => { + if (a.rBin === confPath) { + return -1; + } + if (b.rBin === confPath) { + return 1; + } + return 0; + })[0]; + this.workspaceExecutables.set(workspacePath, exec); + return; + } + } + + // fallback to a configured path if it exists + if (confPath && validateRExecutablePath(confPath)) { + console.log(`[RExecutableService] Executable set to configuration path: ${confPath}`); + const exec = this.executableFactory.create(confPath); + this.workspaceExecutables.set(workspacePath, exec); + } + } + } + } else { + // todo + } + } } + + diff --git a/src/executables/service/locator/index.ts b/src/executables/service/locator/index.ts index b7925dec1..68f3336d3 100644 --- a/src/executables/service/locator/index.ts +++ b/src/executables/service/locator/index.ts @@ -4,6 +4,8 @@ import { UnixExecLocator } from './unix'; import { WindowsExecLocator } from './windows'; import { AbstractLocatorService } from './shared'; + + export class LocatorServiceFactory { static getLocator(): AbstractLocatorService { if (process.platform === 'win32') { diff --git a/src/executables/service/locator/shared.ts b/src/executables/service/locator/shared.ts index cdf941f36..d17e3788a 100644 --- a/src/executables/service/locator/shared.ts +++ b/src/executables/service/locator/shared.ts @@ -1,10 +1,12 @@ import { execSync } from 'child_process'; import * as fs from 'fs-extra'; import * as vscode from 'vscode'; +import { normaliseRPathString } from '../../../util'; export function getRDetailsFromPath(rPath: string): {version: string, arch: string} { try { - const child = execSync(`${rPath} --version`).toString(); + const path = normaliseRPathString(rPath); + const child = execSync(`${path} --version`).toString(); const versionRegex = /(?<=R version\s)[0-9.]*/g; const archRegex = /[0-9]*-bit/g; const out = { @@ -59,7 +61,7 @@ export function getUniquePaths(paths: string[]): string[] { export abstract class AbstractLocatorService { protected _binaryPaths: string[]; protected emitter: vscode.EventEmitter; - public abstract refreshPaths(): void; + public abstract refreshPaths(): Promise; public get hasPaths(): boolean { return this._binaryPaths.length > 0; } @@ -69,4 +71,4 @@ export abstract class AbstractLocatorService { public get onDidRefreshPaths(): vscode.Event { return this.emitter.event; } -} \ No newline at end of file +} diff --git a/src/executables/service/locator/unix.ts b/src/executables/service/locator/unix.ts index 07151c581..f185f8679 100644 --- a/src/executables/service/locator/unix.ts +++ b/src/executables/service/locator/unix.ts @@ -11,7 +11,8 @@ export class UnixExecLocator extends AbstractLocatorService { this.emitter = new vscode.EventEmitter(); this._binaryPaths = []; } - public refreshPaths(): void { + // eslint-disable-next-line @typescript-eslint/require-await + public async refreshPaths(): Promise { this._binaryPaths = getUniquePaths(Array.from( new Set([ ...this.getHomeFromDirs(), @@ -69,14 +70,17 @@ export class UnixExecLocator extends AbstractLocatorService { private getHomeFromEnv(): string[] { const envBins: string[] = []; - const os_paths: string[] | string = process.env.PATH.split(';'); + const os_paths: string[] | string | undefined = process?.env?.PATH?.split(';'); - for (const os_path of os_paths) { - const os_r_path: string = path.join(os_path, 'R'); - if (fs.existsSync(os_r_path)) { - envBins.push(os_r_path); + if (os_paths) { + for (const os_path of os_paths) { + const os_r_path: string = path.join(os_path, 'R'); + if (fs.existsSync(os_r_path)) { + envBins.push(os_r_path); + } } } + return envBins; } diff --git a/src/executables/service/locator/windows.ts b/src/executables/service/locator/windows.ts index b5239e2b6..4e2a02660 100644 --- a/src/executables/service/locator/windows.ts +++ b/src/executables/service/locator/windows.ts @@ -10,19 +10,19 @@ export class WindowsExecLocator extends AbstractLocatorService { this.emitter = new vscode.EventEmitter(); this._binaryPaths = []; } - public refreshPaths(): void { + public async refreshPaths(): Promise { this._binaryPaths = getUniquePaths(Array.from( new Set([ ...this.getHomeFromDirs(), ...this.getHomeFromEnv(), - ...this.getHomeFromRegistry(), + ...await this.getHomeFromRegistry(), // ... this.getHomeFromConda() ]) )); this.emitter.fire(this._binaryPaths); } - private getHomeFromRegistry(): string[] { + private async getHomeFromRegistry(): Promise { const registryBins: string[] = []; const potentialBins = [ new winreg({ @@ -36,7 +36,7 @@ export class WindowsExecLocator extends AbstractLocatorService { ]; for (const bin of potentialBins) { - void new Promise( + await new Promise( (c, e) => { bin.get('InstallPath', (err, result) => err === null ? c(result) : e(err)); } @@ -86,14 +86,17 @@ export class WindowsExecLocator extends AbstractLocatorService { private getHomeFromEnv(): string[] { const envBins: string[] = []; - const os_paths: string[] | string = process.env.PATH.split(';'); + const os_paths: string[] | string | undefined = process?.env?.PATH?.split(';'); - for (const os_path of os_paths) { - const os_r_path: string = path.join(os_path, 'R' + '.exe'); - if (fs.existsSync(os_r_path)) { - envBins.push(os_r_path); + if (os_paths) { + for (const os_path of os_paths) { + const os_r_path: string = path.join(os_path, 'R' + '.exe'); + if (fs.existsSync(os_r_path)) { + envBins.push(os_r_path); + } } } + return envBins; } diff --git a/src/executables/service/pathStorage.ts b/src/executables/service/pathStorage.ts index 074d623fb..1979f175a 100644 --- a/src/executables/service/pathStorage.ts +++ b/src/executables/service/pathStorage.ts @@ -13,7 +13,7 @@ export class RExecutablePathStorage { return this.store; } - public setExecutablePath(workingDir: string, binPath: string): void { + public setExecutablePath(workingDir: string, binPath: string | undefined): void { if (binPath) { this.store.set(workingDir, binPath); } else { @@ -22,11 +22,11 @@ export class RExecutablePathStorage { void this.saveStorage(); } - public getActiveExecutablePath(): string { + public getActiveExecutablePath(): string | undefined { return this.store.get(getCurrentWorkspaceFolder().uri.fsPath); } - public getExecutablePath(workingDir: string): string { + public getExecutablePath(workingDir: string): string | undefined { return this.store.get(workingDir); } diff --git a/src/executables/service/registry.ts b/src/executables/service/registry.ts index 6d5a4d2c7..81f005b19 100644 --- a/src/executables/service/registry.ts +++ b/src/executables/service/registry.ts @@ -24,4 +24,8 @@ export class RExecutableRegistry { public hasExecutable(executable: ExecutableType): boolean { return this._executables.has(executable); } + + public getExecutablesWithVersion(version: string): ExecutableType[] { + return [...this._executables.values()].filter((v) => v.rVersion === version); + } } diff --git a/src/executables/service/renv.ts b/src/executables/service/renv.ts new file mode 100644 index 000000000..91b161670 --- /dev/null +++ b/src/executables/service/renv.ts @@ -0,0 +1,22 @@ +import path = require('path'); +import * as fs from 'fs-extra'; + +interface IRenvLock { + 'R': { + 'Version': string, + 'Repositories': Record[] + }; +} + +export function getRenvVersion(workspacePath: string): string { + try { + const lockPath = path.join(workspacePath, 'renv.lock'); + if (!fs.existsSync(lockPath)) { + return ''; + } + const lockContent = fs.readJSONSync(lockPath) as IRenvLock; + return lockContent?.R?.Version ?? ''; + } catch (error) { + return ''; + } +} \ No newline at end of file diff --git a/src/executables/ui/quickpick.ts b/src/executables/ui/quickpick.ts index 7337d7769..dfde8e1c2 100644 --- a/src/executables/ui/quickpick.ts +++ b/src/executables/ui/quickpick.ts @@ -2,9 +2,10 @@ import * as vscode from 'vscode'; import { ExecutableNotifications } from '.'; import { validateRExecutablePath } from '..'; -import { config, getCurrentWorkspaceFolder, getRPathConfigEntry } from '../../util'; +import { config, getCurrentWorkspaceFolder, getRPathConfigEntry, isMultiRoot } from '../../util'; import { isVirtual, ExecutableType } from '../service'; import { RExecutableService } from '../service'; +import { getRenvVersion } from '../service/renv'; class ExecutableQuickPickItem implements vscode.QuickPickItem { public label: string; @@ -14,10 +15,16 @@ class ExecutableQuickPickItem implements vscode.QuickPickItem { public alwaysShow?: boolean; private _executable: ExecutableType; - constructor(executable: ExecutableType) { + constructor(executable: ExecutableType, recommended?: boolean) { this._executable = executable; - this.label = executable.tooltip; this.description = executable.rBin; + + if (recommended) { + this.label = `$(star) ${executable.tooltip}`; + this.detail = 'ffffffffffff'; + } else { + this.label = executable.tooltip; + } } public get executable(): ExecutableType { @@ -34,11 +41,11 @@ enum PathQuickPickMenu { export class ExecutableQuickPick implements vscode.Disposable { private readonly service: RExecutableService; private quickpick: vscode.QuickPick; - private currentFolder: string; + private currentFolder: vscode.WorkspaceFolder; public constructor(service: RExecutableService) { this.service = service; - this.currentFolder = getCurrentWorkspaceFolder().uri.fsPath; + this.currentFolder = getCurrentWorkspaceFolder(); } public dispose(): void { @@ -64,24 +71,28 @@ export class ExecutableQuickPick implements vscode.Disposable { label: PathQuickPickMenu.search, alwaysShow: true, picked: false - }, - { + } + ); + if (configPath) { + qpItems.push({ label: PathQuickPickMenu.configuration, alwaysShow: true, description: configPath, detail: validateRExecutablePath(configPath) ? '' : 'Invalid R folder', picked: false - } - ); + }); + } + + sortBins([...this.service.executables]).forEach((bin: ExecutableType) => { - qpItems.push(new ExecutableQuickPickItem(bin)); + qpItems.push(new ExecutableQuickPickItem(bin, recommendPath(bin, this.currentFolder))); }); this.quickpick.items = qpItems; for (const item of this.quickpick.items) { - if (item.description === this.service.getWorkspaceExecutable(this.currentFolder)?.rBin) { + if (item.description === this.service.getWorkspaceExecutable(this.currentFolder?.uri?.fsPath)?.rBin) { this.quickpick.activeItems = [item]; } } @@ -102,13 +113,13 @@ export class ExecutableQuickPick implements vscode.Disposable { } function setupQuickpickListeners(self: ExecutableQuickPick, resolver: () => void): void { - self.quickpick.onDidTriggerButton((item: vscode.QuickInputButton) => { + self.quickpick.onDidTriggerButton(async (item: vscode.QuickInputButton) => { if (item.tooltip === 'Refresh paths') { - self.service.executablePathLocator.refreshPaths(); + await self.service.executablePathLocator.refreshPaths(); self.setItems(); self.quickpick.show(); } else { - self.service.setWorkspaceExecutable(self.currentFolder, undefined); + self.service.setWorkspaceExecutable(self.currentFolder?.uri?.fsPath, undefined); self.quickpick.hide(); } }); @@ -124,29 +135,29 @@ export class ExecutableQuickPick implements vscode.Disposable { title: ' R executable file' }; void vscode.window.showOpenDialog(opts).then((execPath) => { - if (validateRExecutablePath(execPath[0].fsPath)) { + if (execPath?.[0].fsPath && validateRExecutablePath(execPath[0].fsPath)) { const rExec = self.service.executableFactory.create(execPath[0].fsPath); - self.service.setWorkspaceExecutable(self.currentFolder, rExec); + self.service.setWorkspaceExecutable(self.currentFolder?.uri?.fsPath, rExec); } else { void vscode.window.showErrorMessage(ExecutableNotifications.badFolder); - self.service.setWorkspaceExecutable(self.currentFolder, undefined); + self.service.setWorkspaceExecutable(self.currentFolder?.uri?.fsPath, undefined); } }); break; } case PathQuickPickMenu.configuration: { const configPath = config().get(getRPathConfigEntry()); - if (validateRExecutablePath(configPath)) { + if (configPath && validateRExecutablePath(configPath)) { const rExec = self.service.executableFactory.create(configPath); - self.service.setWorkspaceExecutable(self.currentFolder, rExec); + self.service.setWorkspaceExecutable(self.currentFolder?.uri?.fsPath, rExec); } else { void vscode.window.showErrorMessage(ExecutableNotifications.badConfig); - self.service.setWorkspaceExecutable(self.currentFolder, undefined); + self.service.setWorkspaceExecutable(self.currentFolder?.uri?.fsPath, undefined); } break; } default: { - self.service.setWorkspaceExecutable(self.currentFolder, (qpItem as ExecutableQuickPickItem).executable); + self.service.setWorkspaceExecutable(self.currentFolder?.uri?.fsPath, (qpItem as ExecutableQuickPickItem).executable); break; } } @@ -160,7 +171,7 @@ export class ExecutableQuickPick implements vscode.Disposable { setupQuickpickOpts(this); setupQuickpickListeners(this, res); void showWorkspaceFolderQP().then((folder: vscode.WorkspaceFolder) => { - this.currentFolder = folder.uri.fsPath; + this.currentFolder = folder; this.setItems(); this.quickpick.show(); }); @@ -168,14 +179,41 @@ export class ExecutableQuickPick implements vscode.Disposable { } } -async function showWorkspaceFolderQP() { +async function showWorkspaceFolderQP(): Promise { const opts: vscode.WorkspaceFolderPickOptions = { ignoreFocusOut: true, placeHolder: 'Select a workspace folder to define an R path for' }; - if (vscode.workspace.workspaceFolders.length > 1) { + if (isMultiRoot()) { return await vscode.window.showWorkspaceFolderPick(opts); - } else { - return vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor.document.uri); } + + const currentDocument = vscode?.window?.activeTextEditor?.document?.uri; + if (currentDocument) { + const folder = vscode.workspace.getWorkspaceFolder(currentDocument); + if (folder) { + return folder; + } else { + return { + index: 0, + uri: currentDocument, + name: 'untitled' + }; + } + } +} + +function recommendPath(executable: ExecutableType, workspaceFolder: vscode.WorkspaceFolder): boolean { + const renvVersion = getRenvVersion(workspaceFolder?.uri?.fsPath); + if (renvVersion) { + console.log(renvVersion); + const compatibleBin = renvVersion === executable.rVersion; + if (compatibleBin) { + return true; + } + + } + const uri = vscode.Uri.file(executable.rBin); + const possibleWorkspace = vscode.workspace.getWorkspaceFolder(uri); + return possibleWorkspace && possibleWorkspace === workspaceFolder; } \ No newline at end of file diff --git a/src/executables/ui/status.ts b/src/executables/ui/status.ts index 40fe49872..86441b6f1 100644 --- a/src/executables/ui/status.ts +++ b/src/executables/ui/status.ts @@ -29,13 +29,14 @@ export class ExecutableStatusItem implements vscode.Disposable { } public refresh(): void { - const execState = this.service.activeExecutable; + const execState = this.service?.activeExecutable; if (execState) { this.languageStatusItem.severity = vscode.LanguageStatusSeverity.Information; this.languageStatusItem.detail = execState.rBin; if (isVirtual(execState)) { const versionString = execState.rVersion ? ` (${execState.rVersion})` : ''; - this.languageStatusItem.text = `${execState.name}${versionString}`; + const name = execState.name ? execState.name : ''; + this.languageStatusItem.text = `${name}${versionString}`; } else { this.languageStatusItem.text = execState.rVersion; } diff --git a/src/extension.ts b/src/extension.ts index fa1e2710c..4ea224630 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -52,7 +52,7 @@ export async function activate(context: vscode.ExtensionContext): Promise('sessionWatcher'); diff --git a/src/util.ts b/src/util.ts index 53df73f46..92b985d66 100644 --- a/src/util.ts +++ b/src/util.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import * as cp from 'child_process'; import { rGuestService, isGuestSession } from './liveShare'; -import { extensionContext, rExecService } from './extension'; +import { extensionContext } from './extension'; export function config(): vscode.WorkspaceConfiguration { return vscode.workspace.getConfiguration('r'); @@ -68,13 +68,6 @@ export function getRPathConfigEntry(term: boolean = false): string { } export async function getRpath(quote = false, overwriteConfig?: string): Promise { - const execPath = rExecService.activeExecutable?.rBin; - if (execPath) { - return execPath; - } else { - return ''; - } - let rpath = ''; // try the config entry specified in the function arg: @@ -305,6 +298,10 @@ export function getRLibPaths(): string { return config().get('libPaths').join('\n'); } +export function normaliseRPathString(path: string): string { + return path.replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1'); +} + // executes an R command returns its output to stdout // uses a regex to filter out output generated e.g. by code in .Rprofile // returns the provided fallback when the command failes @@ -525,3 +522,12 @@ export async function promptToInstallRPackage(name: string, section: string, cwd } }); } + +export function isMultiRoot(): boolean { + const folders = vscode?.workspace?.workspaceFolders; + if (folders) { + return folders.length > 1; + } else { + return false; + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index e4d68719f..c5667e181 100644 --- a/yarn.lock +++ b/yarn.lock @@ -253,10 +253,10 @@ resolved "https://registry.yarnpkg.com/@types/text-table/-/text-table-0.2.1.tgz#39c4d4a058a82f677392dfd09976e83d9b4c9264" integrity sha512-dchbFCWfVgUSWEvhOkXGS7zjm+K7jCUvGrQkAHPk2Fmslfofp4HQTH2pqnQ3Pw5GPYv0zWa2AQjKtsfZThuemQ== -"@types/vscode@^1.60.0": - version "1.68.0" - resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.68.0.tgz#c0864e4ea43c509bfc6b53f4e91aa923fd0475b8" - integrity sha512-duBwEK5ta/eBBMJMQ7ECMEsMvlE3XJdRGh3xoS1uOO4jl2Z4LPBl5vx8WvBP10ERAgDRmIt/FaSD4RHyBGbChw== +"@types/vscode@^1.65.0": + version "1.69.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.69.0.tgz#a472011af392fbcf82cbb82f60b4c239c21b921c" + integrity sha512-RlzDAnGqUoo9wS6d4tthNyAdZLxOIddLiX3djMoWk29jFfSA1yJbIwr0epBYqqYarWB6s2Z+4VaZCQ80Jaa3kA== "@types/winreg@^1.2.31": version "1.2.31" From f5a6075eb651a99a08c716f3e6e4a0293dc576d1 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Thu, 15 Jul 2021 18:07:20 +1000 Subject: [PATCH 06/22] Update r.rmarkdown.codeLensCommands Use an enum to restrict the input for the setting: r.rmarkdown.codeLensCommands --- package.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 07de3b608..545858fab 100644 --- a/package.json +++ b/package.json @@ -1327,7 +1327,19 @@ "r.libPaths": { "type": "array", "items": { - "type": "string" + "type": "string", + "enum": [ + "r.selectCurrentChunk", + "r.runCurrentChunk", + "r.runAboveChunks", + "r.runCurrentAndBelowChunks", + "r.runBelowChunks", + "r.runAllChunks", + "r.runPreviousChunk", + "r.runNextChunk", + "r.goToPreviousChunk", + "r.goToNextChunk" + ] }, "default": [], "markdownDescription": "Additional library paths to launch R background processes (R languageserver, help server, etc.). These paths will be appended to `.libPaths()` on process startup. It could be useful for projects with [renv](https://rstudio.github.io/renv/index.html) enabled." From fe5e42f80127fd537b53991a7763b5ab2ae714ce Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Mon, 11 Jul 2022 16:24:23 +1000 Subject: [PATCH 07/22] Fix rebase bug, lint error Enum change was pushed to package.json for some reason --- package.json | 14 +------------- src/executables/ui/status.ts | 4 ++-- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 545858fab..07de3b608 100644 --- a/package.json +++ b/package.json @@ -1327,19 +1327,7 @@ "r.libPaths": { "type": "array", "items": { - "type": "string", - "enum": [ - "r.selectCurrentChunk", - "r.runCurrentChunk", - "r.runAboveChunks", - "r.runCurrentAndBelowChunks", - "r.runBelowChunks", - "r.runAllChunks", - "r.runPreviousChunk", - "r.runNextChunk", - "r.goToPreviousChunk", - "r.goToNextChunk" - ] + "type": "string" }, "default": [], "markdownDescription": "Additional library paths to launch R background processes (R languageserver, help server, etc.). These paths will be appended to `.libPaths()` on process startup. It could be useful for projects with [renv](https://rstudio.github.io/renv/index.html) enabled." diff --git a/src/executables/ui/status.ts b/src/executables/ui/status.ts index 86441b6f1..94d976009 100644 --- a/src/executables/ui/status.ts +++ b/src/executables/ui/status.ts @@ -8,7 +8,7 @@ enum BinText { missing = '$(warning) Select executable' } -export class ExecutableStatusItem implements vscode.Disposable { +export class ExecutableStatusItem implements vscode.Disposable { private readonly service: RExecutableService; private languageStatusItem: vscode.LanguageStatusItem; @@ -26,7 +26,7 @@ export class ExecutableStatusItem implements vscode.Disposable { public constructor(service: RExecutableService) { this.service = service; this.createItem(); - } + } public refresh(): void { const execState = this.service?.activeExecutable; From a2fe14a5f9384f6d656fe8c08833f3f7c8b53e2b Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Wed, 13 Jul 2022 12:23:47 +1000 Subject: [PATCH 08/22] Improve quickpick categories QP items now grouped into - recommended - virtual - global - Changed various functions into arrow functions to pass `'this' --- src/executables/service/locator/shared.ts | 2 +- src/executables/service/locator/unix.ts | 22 +-- src/executables/service/pathStorage.ts | 6 +- src/executables/service/renv.ts | 27 +++- src/executables/ui/quickpick.ts | 161 ++++++++++++++-------- 5 files changed, 145 insertions(+), 73 deletions(-) diff --git a/src/executables/service/locator/shared.ts b/src/executables/service/locator/shared.ts index d17e3788a..a9374fd1c 100644 --- a/src/executables/service/locator/shared.ts +++ b/src/executables/service/locator/shared.ts @@ -6,7 +6,7 @@ import { normaliseRPathString } from '../../../util'; export function getRDetailsFromPath(rPath: string): {version: string, arch: string} { try { const path = normaliseRPathString(rPath); - const child = execSync(`${path} --version`).toString(); + const child = execSync(`${path} --version`)?.toString(); const versionRegex = /(?<=R version\s)[0-9.]*/g; const archRegex = /[0-9]*-bit/g; const out = { diff --git a/src/executables/service/locator/unix.ts b/src/executables/service/locator/unix.ts index f185f8679..9c2184235 100644 --- a/src/executables/service/locator/unix.ts +++ b/src/executables/service/locator/unix.ts @@ -49,16 +49,18 @@ export class UnixExecLocator extends AbstractLocatorService { ]; for (const dir of conda_dirs) { if (fs.existsSync(dir)) { - const lines = fs.readFileSync(dir).toString(); - for (const line of lines.split('\n')) { - if (line) { - const potential_dirs = [ - `${line}/lib64/R/bin/R`, - `${line}/lib/R/bin/R` - ]; - for (const dir of potential_dirs) { - if (fs.existsSync(dir)) { - dirBins.push(dir); + const lines = fs.readFileSync(dir)?.toString(); + if (lines) { + for (const line of lines.split('\n')) { + if (line) { + const potential_dirs = [ + `${line}/lib64/R/bin/R`, + `${line}/lib/R/bin/R` + ]; + for (const dir of potential_dirs) { + if (fs.existsSync(dir)) { + dirBins.push(dir); + } } } } diff --git a/src/executables/service/pathStorage.ts b/src/executables/service/pathStorage.ts index 1979f175a..152e3b764 100644 --- a/src/executables/service/pathStorage.ts +++ b/src/executables/service/pathStorage.ts @@ -40,7 +40,11 @@ export class RExecutablePathStorage { } private mapToString(map: Map): string { - return JSON.stringify([...map]); + try { + return JSON.stringify([...map]); + } catch (error) { + return ''; + } } private stringToMap(str: string): Map { diff --git a/src/executables/service/renv.ts b/src/executables/service/renv.ts index 91b161670..3d787693a 100644 --- a/src/executables/service/renv.ts +++ b/src/executables/service/renv.ts @@ -8,15 +8,28 @@ interface IRenvLock { }; } -export function getRenvVersion(workspacePath: string): string { - try { - const lockPath = path.join(workspacePath, 'renv.lock'); - if (!fs.existsSync(lockPath)) { +export function getRenvVersion(workspacePath: string): string | undefined { + if (isRenvWorkspace(workspacePath)) { + try { + const lockPath = path.join(workspacePath, 'renv.lock'); + if (!fs.existsSync(lockPath)) { + return ''; + } + const lockContent = fs.readJSONSync(lockPath) as IRenvLock; + return lockContent?.R?.Version ?? ''; + } catch (error) { return ''; } - const lockContent = fs.readJSONSync(lockPath) as IRenvLock; - return lockContent?.R?.Version ?? ''; + } else { + return undefined; + } +} + +export function isRenvWorkspace(workspacePath: string): boolean { + try { + const renvPath = path.join(workspacePath, 'renv'); + return fs.existsSync(renvPath); } catch (error) { - return ''; + return false; } } \ No newline at end of file diff --git a/src/executables/ui/quickpick.ts b/src/executables/ui/quickpick.ts index dfde8e1c2..a38d7c81f 100644 --- a/src/executables/ui/quickpick.ts +++ b/src/executables/ui/quickpick.ts @@ -8,6 +8,8 @@ import { RExecutableService } from '../service'; import { getRenvVersion } from '../service/renv'; class ExecutableQuickPickItem implements vscode.QuickPickItem { + public recommended: boolean; + public category: string; public label: string; public description: string; public detail?: string; @@ -15,13 +17,20 @@ class ExecutableQuickPickItem implements vscode.QuickPickItem { public alwaysShow?: boolean; private _executable: ExecutableType; - constructor(executable: ExecutableType, recommended?: boolean) { + constructor(executable: ExecutableType, recommended: boolean) { this._executable = executable; this.description = executable.rBin; + if (isVirtual(executable)) { + this.category = 'Virtual'; + } else { + this.category = 'Global'; + } + + this.recommended = recommended; + if (recommended) { this.label = `$(star) ${executable.tooltip}`; - this.detail = 'ffffffffffff'; } else { this.label = executable.tooltip; } @@ -53,17 +62,6 @@ export class ExecutableQuickPick implements vscode.Disposable { } private setItems(): void { - function sortBins(bins: ExecutableType[]) { - return bins.sort((a, b) => { - if (!isVirtual(a) && isVirtual(b)) { - return 1; - } else if (!isVirtual(b) && isVirtual(a)) { - return -1; - } else { - return a.rVersion.localeCompare(b.rVersion, undefined, { numeric: true, sensitivity: 'base' }); - } - }); - } const qpItems: vscode.QuickPickItem[] = []; const configPath = config().get(getRPathConfigEntry()); qpItems.push( @@ -83,14 +81,44 @@ export class ExecutableQuickPick implements vscode.Disposable { }); } + const renvVersion = getRenvVersion(this.currentFolder.uri.fsPath) ?? undefined; + const recommendedItems: vscode.QuickPickItem[] = [ + { + label: 'Recommended', + kind: vscode.QuickPickItemKind.Separator + } + ]; + const virtualItems: vscode.QuickPickItem[] = [ + { + label: 'Virtual', + kind: vscode.QuickPickItemKind.Separator + } + ]; + const globalItems: vscode.QuickPickItem[] = [ + { + label: 'Global', + kind: vscode.QuickPickItemKind.Separator + } + ]; - - sortBins([...this.service.executables]).forEach((bin: ExecutableType) => { - qpItems.push(new ExecutableQuickPickItem(bin, recommendPath(bin, this.currentFolder))); + [...this.service.executables].forEach((v) => { + const item = new ExecutableQuickPickItem(v, recommendPath(v, this.currentFolder, renvVersion)); + if (item.recommended) { + recommendedItems.push(item); + } else { + switch (item.category) { + case 'Virtual': { + virtualItems.push(item); + break; + } + case 'Global': { + globalItems.push(item); + break; + } + } + } }); - - this.quickpick.items = qpItems; - + this.quickpick.items = [...qpItems, ...recommendedItems, ...virtualItems, ...globalItems]; for (const item of this.quickpick.items) { if (item.description === this.service.getWorkspaceExecutable(this.currentFolder?.uri?.fsPath)?.rBin) { this.quickpick.activeItems = [item]; @@ -98,32 +126,42 @@ export class ExecutableQuickPick implements vscode.Disposable { } } + /** + * @description + * Basic display of the quickpick is: + * - Manual executable selection + * - Configuration path (may be hidden) + * - Recommended paths (may be hidden) + * - Virtual paths + * - Global paths + * @returns {*} {Promise} + * @memberof ExecutableQuickPick + */ public async showQuickPick(): Promise { - function setupQuickpickOpts(self: ExecutableQuickPick): void { - self.quickpick = vscode.window.createQuickPick(); - self.quickpick.title = 'Select R executable path'; - self.quickpick.canSelectMany = false; - self.quickpick.ignoreFocusOut = true; - self.quickpick.matchOnDescription = true; - self.quickpick.placeholder = ''; - self.quickpick.buttons = [ + const setupQuickpickOpts = () => { + this.quickpick = vscode.window.createQuickPick(); + this.quickpick.title = 'Select R executable path'; + this.quickpick.canSelectMany = false; + this.quickpick.ignoreFocusOut = true; + this.quickpick.matchOnDescription = true; + this.quickpick.buttons = [ { iconPath: new vscode.ThemeIcon('clear-all'), tooltip: 'Clear stored path' }, { iconPath: new vscode.ThemeIcon('refresh'), tooltip: 'Refresh paths' } ]; - } + }; - function setupQuickpickListeners(self: ExecutableQuickPick, resolver: () => void): void { - self.quickpick.onDidTriggerButton(async (item: vscode.QuickInputButton) => { + const setupQuickpickListeners = (resolver: () => void) => { + this.quickpick.onDidTriggerButton(async (item: vscode.QuickInputButton) => { if (item.tooltip === 'Refresh paths') { - await self.service.executablePathLocator.refreshPaths(); - self.setItems(); - self.quickpick.show(); + await this.service.executablePathLocator.refreshPaths(); + this.setItems(); + this.quickpick.show(); } else { - self.service.setWorkspaceExecutable(self.currentFolder?.uri?.fsPath, undefined); - self.quickpick.hide(); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, undefined); + this.quickpick.hide(); } }); - self.quickpick.onDidChangeSelection((items: vscode.QuickPickItem[]) => { + this.quickpick.onDidChangeSelection((items: vscode.QuickPickItem[]) => { const qpItem = items[0]; if (qpItem.label) { switch (qpItem.label) { @@ -136,11 +174,11 @@ export class ExecutableQuickPick implements vscode.Disposable { }; void vscode.window.showOpenDialog(opts).then((execPath) => { if (execPath?.[0].fsPath && validateRExecutablePath(execPath[0].fsPath)) { - const rExec = self.service.executableFactory.create(execPath[0].fsPath); - self.service.setWorkspaceExecutable(self.currentFolder?.uri?.fsPath, rExec); + const rExec = this.service.executableFactory.create(execPath[0].fsPath); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, rExec); } else { void vscode.window.showErrorMessage(ExecutableNotifications.badFolder); - self.service.setWorkspaceExecutable(self.currentFolder?.uri?.fsPath, undefined); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, undefined); } }); break; @@ -148,30 +186,36 @@ export class ExecutableQuickPick implements vscode.Disposable { case PathQuickPickMenu.configuration: { const configPath = config().get(getRPathConfigEntry()); if (configPath && validateRExecutablePath(configPath)) { - const rExec = self.service.executableFactory.create(configPath); - self.service.setWorkspaceExecutable(self.currentFolder?.uri?.fsPath, rExec); + const rExec = this.service.executableFactory.create(configPath); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, rExec); } else { void vscode.window.showErrorMessage(ExecutableNotifications.badConfig); - self.service.setWorkspaceExecutable(self.currentFolder?.uri?.fsPath, undefined); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, undefined); } break; } default: { - self.service.setWorkspaceExecutable(self.currentFolder?.uri?.fsPath, (qpItem as ExecutableQuickPickItem).executable); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, (qpItem as ExecutableQuickPickItem).executable); break; } } } - self.quickpick.hide(); + this.quickpick.hide(); resolver(); }); - } + }; return await new Promise((res) => { - setupQuickpickOpts(this); - setupQuickpickListeners(this, res); + setupQuickpickOpts(); + setupQuickpickListeners(res); void showWorkspaceFolderQP().then((folder: vscode.WorkspaceFolder) => { this.currentFolder = folder; + const currentExec = this.service.getWorkspaceExecutable(folder?.uri?.fsPath); + if (currentExec) { + this.quickpick.placeholder = `Current path: ${currentExec.rBin}`; + } else { + this.quickpick.placeholder = ''; + } this.setItems(); this.quickpick.show(); }); @@ -179,16 +223,25 @@ export class ExecutableQuickPick implements vscode.Disposable { } } -async function showWorkspaceFolderQP(): Promise { +async function showWorkspaceFolderQP(): Promise { const opts: vscode.WorkspaceFolderPickOptions = { ignoreFocusOut: true, placeHolder: 'Select a workspace folder to define an R path for' }; + const currentDocument = vscode?.window?.activeTextEditor?.document?.uri; if (isMultiRoot()) { - return await vscode.window.showWorkspaceFolderPick(opts); + const workspaceFolder = await vscode.window.showWorkspaceFolderPick(opts); + if (workspaceFolder) { + return workspaceFolder; + } else if (currentDocument) { + return { + index: 0, + uri: currentDocument, + name: 'untitled' + }; + } } - const currentDocument = vscode?.window?.activeTextEditor?.document?.uri; if (currentDocument) { const folder = vscode.workspace.getWorkspaceFolder(currentDocument); if (folder) { @@ -201,12 +254,12 @@ async function showWorkspaceFolderQP(): Promise { }; } } + + return undefined; } -function recommendPath(executable: ExecutableType, workspaceFolder: vscode.WorkspaceFolder): boolean { - const renvVersion = getRenvVersion(workspaceFolder?.uri?.fsPath); +function recommendPath(executable: ExecutableType, workspaceFolder: vscode.WorkspaceFolder, renvVersion?: string): boolean { if (renvVersion) { - console.log(renvVersion); const compatibleBin = renvVersion === executable.rVersion; if (compatibleBin) { return true; @@ -215,5 +268,5 @@ function recommendPath(executable: ExecutableType, workspaceFolder: vscode.Works } const uri = vscode.Uri.file(executable.rBin); const possibleWorkspace = vscode.workspace.getWorkspaceFolder(uri); - return possibleWorkspace && possibleWorkspace === workspaceFolder; + return !!possibleWorkspace && possibleWorkspace === workspaceFolder; } \ No newline at end of file From 403dd48b4a339a138346a272960bf910af7fb666 Mon Sep 17 00:00:00 2001 From: ElianHugh Date: Thu, 14 Jul 2022 13:52:03 +1000 Subject: [PATCH 09/22] WIP windows support --- src/executables/index.ts | 9 +++-- src/executables/service/locator/shared.ts | 8 ++--- src/executables/service/locator/windows.ts | 38 +++++++++++++--------- src/executables/ui/quickpick.ts | 15 ++++++--- 4 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/executables/index.ts b/src/executables/index.ts index 9240cbbbd..6d2282b5a 100644 --- a/src/executables/index.ts +++ b/src/executables/index.ts @@ -112,6 +112,11 @@ export class RExecutableManager implements vscode.Disposable { * @returns boolean */ export function validateRExecutablePath(execPath: string): boolean { - const basename = process.platform === 'win32' ? 'R.exe' : 'R'; - return fs.existsSync(execPath) && (path.basename(execPath) === basename); + try { + const basename = process.platform === 'win32' ? 'R.exe' : 'R'; + fs.accessSync(execPath, fs.constants.X_OK); + return (path.basename(execPath) === basename); + } catch (error) { + return false; + } } diff --git a/src/executables/service/locator/shared.ts b/src/executables/service/locator/shared.ts index a9374fd1c..6473530f8 100644 --- a/src/executables/service/locator/shared.ts +++ b/src/executables/service/locator/shared.ts @@ -1,13 +1,13 @@ -import { execSync } from 'child_process'; +import { spawnSync } from 'child_process'; import * as fs from 'fs-extra'; import * as vscode from 'vscode'; import { normaliseRPathString } from '../../../util'; -export function getRDetailsFromPath(rPath: string): {version: string, arch: string} { +export function getRDetailsFromPath(rPath: string): { version: string, arch: string } { try { const path = normaliseRPathString(rPath); - const child = execSync(`${path} --version`)?.toString(); - const versionRegex = /(?<=R version\s)[0-9.]*/g; + const child = spawnSync(path, [`--version`]).output.join('\n'); + const versionRegex = /(?<=R\sversion\s)[0-9.]*/g; const archRegex = /[0-9]*-bit/g; const out = { version: child.match(versionRegex)?.[0] ?? '', diff --git a/src/executables/service/locator/windows.ts b/src/executables/service/locator/windows.ts index 4e2a02660..f91807d8d 100644 --- a/src/executables/service/locator/windows.ts +++ b/src/executables/service/locator/windows.ts @@ -3,6 +3,15 @@ import * as vscode from 'vscode'; import path = require('path'); import winreg = require('winreg'); import { getUniquePaths, AbstractLocatorService } from './shared'; +import { validateRExecutablePath } from '../..'; + +const WindowsKnownPaths = [ + path.join(process.env.ProgramFiles, 'R'), + path.join(process.env['ProgramFiles(x86)'], 'R'), + path.join(process.env.ProgramFiles, 'Microsoft', 'R Open'), + path.join(process.env.ProgramFiles, 'Microsoft', 'R Open'), +]; + export class WindowsExecLocator extends AbstractLocatorService { constructor() { @@ -62,22 +71,19 @@ export class WindowsExecLocator extends AbstractLocatorService { private getHomeFromDirs(): string[] { const dirBins: string[] = []; - const potential_bin_paths: string[] = [ - '%ProgramFiles%\\R\\', - '%ProgramFiles(x86)%\\R\\' - ]; - for (const bin of potential_bin_paths) { - const resolvedBin = path.resolve(bin); - if (fs.existsSync(resolvedBin)) { - const i386 = `${resolvedBin}\\i386\\`; - const x64 = `${resolvedBin}\\x64\\`; - - if (fs.existsSync(i386)) { - dirBins.push(i386); - } + for (const bin of WindowsKnownPaths) { + if (fs.existsSync(bin)) { + const dirs = fs.readdirSync(bin); + for (const dir of dirs) { + const i386 = `${bin}\\${dir}\\bin\\i386\\R.exe`; + const x64 = `${bin}\\${dir}\\bin\\x64\\R.exe`; + if (validateRExecutablePath(i386)) { + dirBins.push(i386); + } - if (fs.existsSync(x64)) { - dirBins.push(x64); + if (validateRExecutablePath(x64)) { + dirBins.push(x64); + } } } } @@ -90,7 +96,7 @@ export class WindowsExecLocator extends AbstractLocatorService { if (os_paths) { for (const os_path of os_paths) { - const os_r_path: string = path.join(os_path, 'R' + '.exe'); + const os_r_path: string = path.join(os_path, '/R.exe'); if (fs.existsSync(os_r_path)) { envBins.push(os_r_path); } diff --git a/src/executables/ui/quickpick.ts b/src/executables/ui/quickpick.ts index a38d7c81f..28a23e9f3 100644 --- a/src/executables/ui/quickpick.ts +++ b/src/executables/ui/quickpick.ts @@ -1,3 +1,4 @@ +import path = require('path'); import * as vscode from 'vscode'; import { ExecutableNotifications } from '.'; @@ -64,6 +65,9 @@ export class ExecutableQuickPick implements vscode.Disposable { private setItems(): void { const qpItems: vscode.QuickPickItem[] = []; const configPath = config().get(getRPathConfigEntry()); + const sortExecutables = (a: ExecutableType, b: ExecutableType) => { + return -a.rVersion.localeCompare(b.rVersion, undefined, { numeric: true, sensitivity: 'base' }); + }; qpItems.push( { label: PathQuickPickMenu.search, @@ -101,7 +105,7 @@ export class ExecutableQuickPick implements vscode.Disposable { } ]; - [...this.service.executables].forEach((v) => { + [...this.service.executables].sort(sortExecutables).forEach((v) => { const item = new ExecutableQuickPickItem(v, recommendPath(v, this.currentFolder, renvVersion)); if (item.recommended) { recommendedItems.push(item); @@ -118,6 +122,8 @@ export class ExecutableQuickPick implements vscode.Disposable { } } }); + + this.quickpick.items = [...qpItems, ...recommendedItems, ...virtualItems, ...globalItems]; for (const item of this.quickpick.items) { if (item.description === this.service.getWorkspaceExecutable(this.currentFolder?.uri?.fsPath)?.rBin) { @@ -172,9 +178,10 @@ export class ExecutableQuickPick implements vscode.Disposable { canSelectMany: false, title: ' R executable file' }; - void vscode.window.showOpenDialog(opts).then((execPath) => { - if (execPath?.[0].fsPath && validateRExecutablePath(execPath[0].fsPath)) { - const rExec = this.service.executableFactory.create(execPath[0].fsPath); + void vscode.window.showOpenDialog(opts).then((epath) => { + const execPath = path.normalize(epath?.[0].fsPath); + if (execPath && validateRExecutablePath(execPath)) { + const rExec = this.service.executableFactory.create(execPath); this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, rExec); } else { void vscode.window.showErrorMessage(ExecutableNotifications.badFolder); From 9861289c3735cf1af935543e9e8303b0c9b4063e Mon Sep 17 00:00:00 2001 From: ElianHugh Date: Fri, 15 Jul 2022 13:40:56 +1000 Subject: [PATCH 10/22] Windows conda, status bar errors, refactoring --- src/executables/conda.ts | 85 ++++++++++++++ src/executables/index.ts | 23 +--- src/executables/service/class.ts | 83 +++++++------- src/executables/service/index.ts | 16 +-- src/executables/service/locator/shared.ts | 8 +- src/executables/service/locator/unix.ts | 69 +++++------ src/executables/service/locator/windows.ts | 126 +++++++++++++-------- src/executables/service/registry.ts | 2 +- src/executables/service/renv.ts | 8 +- src/executables/service/types.ts | 29 +++++ src/executables/ui/index.ts | 5 - src/executables/ui/quickpick.ts | 18 +-- src/executables/ui/status.ts | 14 ++- src/executables/ui/types.ts | 4 + 14 files changed, 308 insertions(+), 182 deletions(-) create mode 100644 src/executables/conda.ts create mode 100644 src/executables/service/types.ts create mode 100644 src/executables/ui/types.ts diff --git a/src/executables/conda.ts b/src/executables/conda.ts new file mode 100644 index 000000000..715b4827e --- /dev/null +++ b/src/executables/conda.ts @@ -0,0 +1,85 @@ + +import { IExecutableDetails } from './service'; +import * as fs from 'fs-extra'; +import * as vscode from 'vscode'; +import path = require('path'); +import { spawn } from 'child_process'; + +export function environmentIsActive(name: string): boolean { + return process.env.CONDA_DEFAULT_ENV === name || + process.env.CONDA_PREFIX === name; +} + +export function getCondaName(executablePath: string): string { + return path.basename(path.dirname(getCondaMetaDir(executablePath))); +} + +export function getCondaMetaDir(executablePath: string): string { + let envDir: string = executablePath; + for (let index = 0; index < 4; index++) { + envDir = path.dirname(envDir); + } + return path.join(envDir, 'conda-meta'); +} + +export function getCondaHistoryPath(executablePath: string): string { + return path.join(getCondaMetaDir(executablePath), 'history'); +} + +export function getCondaActivationScript(executablePath: string): string { + const envDir = path.dirname(getCondaMetaDir(executablePath)); + return path.join(path.dirname(path.dirname(envDir)), 'Scripts', 'activate'); +} + +export function isCondaInstallation(executablePath: string): boolean { + console.log(getCondaName(executablePath)); + return fs.existsSync(getCondaMetaDir(executablePath)); +} + +export function getRDetailsFromMetaHistory(executablePath: string): IExecutableDetails { + try { + + const reg = new RegExp(/([0-9]{2})::r-base-([0-9.]*)/g); + const historyContent = fs.readFileSync(getCondaHistoryPath(executablePath))?.toString(); + const res = reg.exec(historyContent); + return { + arch: res?.[1] ? `${res[1]}-bit` : '', + version: res?.[2] ? res[2] : '' + }; + } catch (error) { + return { + arch: '', + version: '' + }; + } + +} + +export function activateCondaEnvironment(executablePath: string): Promise { + return new Promise((resolve, reject) => { + try { + const opts = { + env: process.env, + shell: true + }; + const activationPath = (getCondaActivationScript(executablePath)); + const commands = [ + activationPath, + `conda activate ${getCondaName(executablePath)}` + ].join(' & '); + const childProc = spawn( + commands, + undefined, + opts + ); + childProc.on('exit', () => resolve(true)); + childProc.on('error', (err) => { + void vscode.window.showErrorMessage(`Error when activating conda environment: ${err.message}`); + reject(false); + }); + } catch (error) { + void vscode.window.showErrorMessage(`Error when activating conda environment: ${error as string}`); + reject(false); + } + }); +} \ No newline at end of file diff --git a/src/executables/index.ts b/src/executables/index.ts index 6d2282b5a..aabd6ef6c 100644 --- a/src/executables/index.ts +++ b/src/executables/index.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import { ExecutableStatusItem, ExecutableQuickPick } from './ui'; import { isVirtual, RExecutableService, ExecutableType, WorkspaceExecutableEvent } from './service'; import { extensionContext } from '../extension'; -import { spawnAsync } from '../util'; +import { activateCondaEnvironment } from './conda'; export { ExecutableType as IRExecutable, VirtualExecutableType as IVirtualRExecutable } from './service'; @@ -81,24 +81,11 @@ export class RExecutableManager implements vscode.Disposable { } - private async activateEnvironment(): Promise { - if (!this.activeExecutable || !isVirtual(this.activeExecutable) || - process.env.CONDA_DEFAULT_ENV !== this.activeExecutable.name) { - return Promise.resolve(); + private async activateEnvironment(): Promise { + if (!this.activeExecutable || !isVirtual(this.activeExecutable)) { + return Promise.resolve(true); } - - const opts = { - env: { - ...process.env - }, - }; - - return spawnAsync( - 'conda', // hard coded for now - this.activeExecutable.activationCommand, - opts, - undefined - ); + return activateCondaEnvironment(this.activeExecutable?.rBin); } } diff --git a/src/executables/service/class.ts b/src/executables/service/class.ts index e1f07fe13..6648092a3 100644 --- a/src/executables/service/class.ts +++ b/src/executables/service/class.ts @@ -1,10 +1,9 @@ +import { getCondaName, getRDetailsFromMetaHistory, isCondaInstallation } from '../conda'; import { getRDetailsFromPath } from './locator'; import { RExecutableRegistry } from './registry'; +import { IExecutableDetails, ExecutableType } from './types'; -export type ExecutableType = RExecutable; -export type VirtualExecutableType = VirtualRExecutable; - -export function isVirtual(executable: RExecutable): executable is VirtualRExecutable { +export function isVirtual(executable: AbstractExecutable): executable is VirtualRExecutable { return executable instanceof VirtualRExecutable; } @@ -16,12 +15,12 @@ export class RExecutableFactory { } public create(executablePath: string): ExecutableType { - const oldExec = [...this.registry.executables.values()].find((v) => v.rBin === executablePath); - if (oldExec) { - return oldExec; + const cachedExec = [...this.registry.executables.values()].find((v) => v.rBin === executablePath); + if (cachedExec) { + return cachedExec; } else { - let executable: RExecutable; - if (new RegExp('\\.conda')?.exec(executablePath)) { + let executable: AbstractExecutable; + if (isCondaInstallation(executablePath)) { executable = new VirtualRExecutable(executablePath); } else { executable = new RExecutable(executablePath); @@ -32,18 +31,10 @@ export class RExecutableFactory { } } -class RExecutable { - private _rBin: string; - private _rVersion: string; - private _arch: string; - - constructor(bin_path: string) { - const details = getRDetailsFromPath(bin_path); - this._rBin = bin_path; - this._rVersion = details.version; - this._arch = details.arch; - } - +export abstract class AbstractExecutable { + protected _rBin: string; + protected _rVersion: string; + protected _rArch: string; public get rBin(): string { return this._rBin; } @@ -53,23 +44,43 @@ class RExecutable { } public get rArch(): string { - return this._arch; + return this._rArch; + } + public abstract tooltip: string; +} + + +export class RExecutable extends AbstractExecutable { + constructor(executablePath: string) { + super(); + const details = getRDetailsFromPath(executablePath); + this._rBin = executablePath; + this._rVersion = details.version; + this._rArch = details.arch; } public get tooltip(): string { - const versionString = this.rVersion ? ` ${this.rVersion}` : ''; - const archString = this.rArch ? ` ${this.rArch}` : ''; - return `R${versionString}${archString}`; + if (this.rVersion && this.rArch) { + return `R ${this.rVersion} ${this.rArch}`; + } + return `$(error) R`; + } + + protected getDetailsFromPath(execPath: string): IExecutableDetails { + return getRDetailsFromPath(execPath); } } -class VirtualRExecutable extends RExecutable { +export class VirtualRExecutable extends AbstractExecutable { private _name: string; - constructor(bin_path: string) { - super(bin_path); - const reg = new RegExp('(?<=\\/envs\\/)(.*?)(?=\\/)'); - this._name = reg?.exec(this.rBin)?.[0] ?? ''; + constructor(executablePath: string) { + super(); + this._name = getCondaName(executablePath); + const details = getRDetailsFromMetaHistory(executablePath); + this._rBin = executablePath; + this._rVersion = details?.version ?? ''; + this._rArch = details?.arch ?? ''; } public get name(): string { @@ -77,15 +88,9 @@ class VirtualRExecutable extends RExecutable { } public get tooltip(): string { - return `${this.name} (${super.tooltip})`; - } - - // todo, hardcoded - public get activationCommand(): string[] { - if (this.name) { - return ['activate', this.name]; - } else { - return ['activate']; + if (this.rVersion && this.rArch) { + return `${this.name} (R ${this.rVersion} ${this.rArch})`; } + return `$(error) ${this.name}`; } } diff --git a/src/executables/service/index.ts b/src/executables/service/index.ts index d127c2915..8e372a5c7 100644 --- a/src/executables/service/index.ts +++ b/src/executables/service/index.ts @@ -1,25 +1,17 @@ import * as vscode from 'vscode'; import { validateRExecutablePath } from '..'; -import { ExecutableType, RExecutableFactory } from './class'; +import { RExecutableFactory } from './class'; import { config, getCurrentWorkspaceFolder, getRPathConfigEntry } from '../../util'; import { RExecutablePathStorage } from './pathStorage'; import { RExecutableRegistry } from './registry'; import { AbstractLocatorService, LocatorServiceFactory } from './locator'; import { getRenvVersion } from './renv'; +import { ExecutableType, WorkspaceExecutableEvent } from './types'; +export * from './types'; export * from './class'; -/** - * @description - * @export - * @interface WorkspaceExecutableEvent - */ -export interface WorkspaceExecutableEvent { - workingFolder: vscode.WorkspaceFolder | undefined, - executable: ExecutableType | undefined -} - /** * @description * @export @@ -49,7 +41,7 @@ export class RExecutableService implements vscode.Disposable { this.workspaceExecutables = new Map(); this.executableEmitter = new vscode.EventEmitter(); this.workspaceEmitter = new vscode.EventEmitter(); - this.executablePathLocator.binaryPaths.forEach((path) => { + this.executablePathLocator.executablePaths.forEach((path) => { this.executableFactory.create(path); }); diff --git a/src/executables/service/locator/shared.ts b/src/executables/service/locator/shared.ts index 6473530f8..19b8dd777 100644 --- a/src/executables/service/locator/shared.ts +++ b/src/executables/service/locator/shared.ts @@ -59,14 +59,14 @@ export function getUniquePaths(paths: string[]): string[] { } export abstract class AbstractLocatorService { - protected _binaryPaths: string[]; + protected _executablePaths: string[]; protected emitter: vscode.EventEmitter; public abstract refreshPaths(): Promise; public get hasPaths(): boolean { - return this._binaryPaths.length > 0; + return this._executablePaths.length > 0; } - public get binaryPaths(): string[] { - return this._binaryPaths; + public get executablePaths(): string[] { + return this._executablePaths; } public get onDidRefreshPaths(): vscode.Event { return this.emitter.event; diff --git a/src/executables/service/locator/unix.ts b/src/executables/service/locator/unix.ts index 9c2184235..ec4c0c018 100644 --- a/src/executables/service/locator/unix.ts +++ b/src/executables/service/locator/unix.ts @@ -9,22 +9,22 @@ export class UnixExecLocator extends AbstractLocatorService { constructor() { super(); this.emitter = new vscode.EventEmitter(); - this._binaryPaths = []; + this._executablePaths = []; } // eslint-disable-next-line @typescript-eslint/require-await public async refreshPaths(): Promise { - this._binaryPaths = getUniquePaths(Array.from( + this._executablePaths = getUniquePaths(Array.from( new Set([ - ...this.getHomeFromDirs(), - ...this.getHomeFromEnv(), - ... this.getHomeFromConda() + ...this.getPathFromDirs(), + ...this.getPathFromEnv(), + ... this.getPathFromConda() ]) )); - this.emitter.fire(this._binaryPaths); + this.emitter.fire(this._executablePaths); } - private getHomeFromDirs(): string[] { - const dirBins: string[] = []; + private getPathFromDirs(): string[] { + const execPaths: string[] = []; const potentialPaths: string[] = [ '/usr/lib64/R/bin/R', '/usr/lib/R/bin/R', @@ -36,30 +36,30 @@ export class UnixExecLocator extends AbstractLocatorService { for (const bin of potentialPaths) { if (fs.existsSync(bin)) { - dirBins.push(bin); + execPaths.push(bin); } } - return dirBins; + return execPaths; } - private getHomeFromConda(): string[] { - const dirBins: string[] = []; - const conda_dirs = [ + private getPathFromConda(): string[] { + const execPaths: string[] = []; + const condaDirs = [ `${os.homedir()}/.conda/environments.txt` ]; - for (const dir of conda_dirs) { - if (fs.existsSync(dir)) { - const lines = fs.readFileSync(dir)?.toString(); + for (const condaEnv of condaDirs) { + if (fs.existsSync(condaEnv)) { + const lines = fs.readFileSync(condaEnv)?.toString(); if (lines) { for (const line of lines.split('\n')) { if (line) { - const potential_dirs = [ + const rDirs = [ `${line}/lib64/R/bin/R`, `${line}/lib/R/bin/R` ]; - for (const dir of potential_dirs) { + for (const dir of rDirs) { if (fs.existsSync(dir)) { - dirBins.push(dir); + execPaths.push(dir); } } } @@ -67,34 +67,23 @@ export class UnixExecLocator extends AbstractLocatorService { } } } - return dirBins; + return execPaths; } - private getHomeFromEnv(): string[] { - const envBins: string[] = []; - const os_paths: string[] | string | undefined = process?.env?.PATH?.split(';'); + private getPathFromEnv(): string[] { + const execPaths: string[] = []; + const osPaths: string[] | string | undefined = process?.env?.PATH?.split(';'); - if (os_paths) { - for (const os_path of os_paths) { - const os_r_path: string = path.join(os_path, 'R'); - if (fs.existsSync(os_r_path)) { - envBins.push(os_r_path); + if (osPaths) { + for (const osPath of osPaths) { + const rPath: string = path.join(osPath, 'R'); + if (fs.existsSync(rPath)) { + execPaths.push(rPath); } } } - return envBins; + return execPaths; } - - // private getHomeFromStorage(): string[] { - // const store = getExecutableStore(); - // const storedBins: string[] = []; - // for (const [_, path] of store) { - // if (fs.existsSync(path) && validateRExecutablePath(path)) { - // storedBins.push(path); - // } - // } - // return storedBins; - // } } diff --git a/src/executables/service/locator/windows.ts b/src/executables/service/locator/windows.ts index f91807d8d..146d8cabc 100644 --- a/src/executables/service/locator/windows.ts +++ b/src/executables/service/locator/windows.ts @@ -1,39 +1,49 @@ import * as fs from 'fs-extra'; import * as vscode from 'vscode'; +import os = require('os'); import path = require('path'); import winreg = require('winreg'); import { getUniquePaths, AbstractLocatorService } from './shared'; import { validateRExecutablePath } from '../..'; -const WindowsKnownPaths = [ - path.join(process.env.ProgramFiles, 'R'), - path.join(process.env['ProgramFiles(x86)'], 'R'), - path.join(process.env.ProgramFiles, 'Microsoft', 'R Open'), - path.join(process.env.ProgramFiles, 'Microsoft', 'R Open'), -]; +const WindowsKnownPaths: string[] = []; + +if (process.env.ProgramFiles) { + WindowsKnownPaths.push( + path.join(process?.env?.ProgramFiles, 'R'), + path.join(process?.env?.ProgramFiles, 'Microsoft', 'R Open') + ); +} + +if (process.env['ProgramFiles(x86)']) { + WindowsKnownPaths.push( + path.join(process?.env?.['ProgramFiles(x86)'], 'R'), + path.join(process?.env?.['ProgramFiles(x86)'], 'Microsoft', 'R Open') + ); +} export class WindowsExecLocator extends AbstractLocatorService { constructor() { super(); this.emitter = new vscode.EventEmitter(); - this._binaryPaths = []; + this._executablePaths = []; } public async refreshPaths(): Promise { - this._binaryPaths = getUniquePaths(Array.from( + this._executablePaths = getUniquePaths(Array.from( new Set([ - ...this.getHomeFromDirs(), - ...this.getHomeFromEnv(), - ...await this.getHomeFromRegistry(), - // ... this.getHomeFromConda() + ...this.getPathFromDirs(), + ...this.getPathFromEnv(), + ...await this.getPathFromRegistry(), + ...this.getPathFromConda() ]) )); - this.emitter.fire(this._binaryPaths); + this.emitter.fire(this._executablePaths); } - private async getHomeFromRegistry(): Promise { - const registryBins: string[] = []; - const potentialBins = [ + private async getPathFromRegistry(): Promise { + const execPaths: string[] = []; + const potentialRegs = [ new winreg({ hive: winreg.HKLM, key: '\\SOFTWARE\\R-core\\R', @@ -44,68 +54,94 @@ export class WindowsExecLocator extends AbstractLocatorService { }) ]; - for (const bin of potentialBins) { + for (const reg of potentialRegs) { await new Promise( (c, e) => { - bin.get('InstallPath', (err, result) => err === null ? c(result) : e(err)); + reg.get('InstallPath', (err, result) => err === null ? c(result) : e(err)); } ).then((item: winreg.RegistryItem) => { if (item) { - const resolvedBin = item.value; - const i386 = `${resolvedBin}\\i386\\`; - const x64 = `${resolvedBin}\\x64\\`; + const resolvedPath = item.value; + const i386 = `${resolvedPath}\\i386\\`; + const x64 = `${resolvedPath}\\x64\\`; if (fs.existsSync(i386)) { - registryBins.push(i386); + execPaths.push(i386); } if (fs.existsSync(x64)) { - registryBins.push(x64); + execPaths.push(x64); } } }); } - return registryBins; + return execPaths; } - private getHomeFromDirs(): string[] { - const dirBins: string[] = []; - for (const bin of WindowsKnownPaths) { - if (fs.existsSync(bin)) { - const dirs = fs.readdirSync(bin); + private getPathFromDirs(): string[] { + const execPaths: string[] = []; + for (const rPath of WindowsKnownPaths) { + if (fs.existsSync(rPath)) { + const dirs = fs.readdirSync(rPath); for (const dir of dirs) { - const i386 = `${bin}\\${dir}\\bin\\i386\\R.exe`; - const x64 = `${bin}\\${dir}\\bin\\x64\\R.exe`; + const i386 = `${rPath}\\${dir}\\bin\\i386\\R.exe`; + const x64 = `${rPath}\\${dir}\\bin\\x64\\R.exe`; + if (validateRExecutablePath(i386)) { - dirBins.push(i386); + execPaths.push(i386); } if (validateRExecutablePath(x64)) { - dirBins.push(x64); + execPaths.push(x64); } } } } - return dirBins; + return execPaths; } - private getHomeFromEnv(): string[] { - const envBins: string[] = []; - const os_paths: string[] | string | undefined = process?.env?.PATH?.split(';'); + private getPathFromEnv(): string[] { + const execPaths: string[] = []; + const osPaths: string[] | string | undefined = process?.env?.PATH?.split(';'); - if (os_paths) { - for (const os_path of os_paths) { - const os_r_path: string = path.join(os_path, '/R.exe'); - if (fs.existsSync(os_r_path)) { - envBins.push(os_r_path); + if (osPaths) { + for (const osPath of osPaths) { + const rPath: string = path.join(osPath, '\\R.exe'); + if (fs.existsSync(rPath)) { + execPaths.push(rPath); } } } - return envBins; + return execPaths; } - // todo - // private getHomeFromConda() {} + private getPathFromConda() { + const execPaths: string[] = []; + const condaDirs = [ + `${os.homedir()}\\.conda\\environments.txt` + ]; + for (const rPath of condaDirs) { + if (fs.existsSync(rPath)) { + const lines = fs.readFileSync(rPath)?.toString(); + if (lines) { + for (const line of lines.split('\r\n')) { + if (line) { + const potentialDirs = [ + `${line}\\lib64\\R\\bin\\R.exe`, + `${line}\\lib\\R\\bin\\R.exe` + ]; + for (const dir of potentialDirs) { + if (fs.existsSync(dir)) { + execPaths.push(dir); + } + } + } + } + } + } + } + return execPaths; + } } diff --git a/src/executables/service/registry.ts b/src/executables/service/registry.ts index 81f005b19..9aa02bc4d 100644 --- a/src/executables/service/registry.ts +++ b/src/executables/service/registry.ts @@ -1,4 +1,4 @@ -import { ExecutableType } from './class'; +import { ExecutableType } from './types'; // necessary to have an executable registry // so that we don't spam the (re)creation of executables diff --git a/src/executables/service/renv.ts b/src/executables/service/renv.ts index 3d787693a..515b89800 100644 --- a/src/executables/service/renv.ts +++ b/src/executables/service/renv.ts @@ -1,13 +1,7 @@ import path = require('path'); +import { IRenvLock } from './types'; import * as fs from 'fs-extra'; -interface IRenvLock { - 'R': { - 'Version': string, - 'Repositories': Record[] - }; -} - export function getRenvVersion(workspacePath: string): string | undefined { if (isRenvWorkspace(workspacePath)) { try { diff --git a/src/executables/service/types.ts b/src/executables/service/types.ts new file mode 100644 index 000000000..67aaa2368 --- /dev/null +++ b/src/executables/service/types.ts @@ -0,0 +1,29 @@ +import * as vscode from 'vscode'; + +import { AbstractExecutable, VirtualRExecutable } from './class'; + +export type ExecutableType = AbstractExecutable; + +export type VirtualExecutableType = VirtualRExecutable; + +export interface IExecutableDetails { + version: string | undefined, + arch: string | undefined +} + +export interface IRenvLock { + 'R': { + 'Version': string, + 'Repositories': Record[] + }; +} + +/** + * @description + * @export + * @interface WorkspaceExecutableEvent + */ +export interface WorkspaceExecutableEvent { + workingFolder: vscode.WorkspaceFolder | undefined, + executable: ExecutableType | undefined +} diff --git a/src/executables/ui/index.ts b/src/executables/ui/index.ts index 6d709e4bc..c404837c2 100644 --- a/src/executables/ui/index.ts +++ b/src/executables/ui/index.ts @@ -1,7 +1,2 @@ export { ExecutableQuickPick } from './quickpick'; export { ExecutableStatusItem } from './status'; - -export enum ExecutableNotifications { - badFolder = 'Supplied R executable path is not a valid R path.', - badConfig = 'Configured path is not a valid R executable path.' -} \ No newline at end of file diff --git a/src/executables/ui/quickpick.ts b/src/executables/ui/quickpick.ts index 28a23e9f3..ad55088ef 100644 --- a/src/executables/ui/quickpick.ts +++ b/src/executables/ui/quickpick.ts @@ -1,7 +1,7 @@ import path = require('path'); import * as vscode from 'vscode'; -import { ExecutableNotifications } from '.'; +import { ExecutableNotifications } from './types'; import { validateRExecutablePath } from '..'; import { config, getCurrentWorkspaceFolder, getRPathConfigEntry, isMultiRoot } from '../../util'; import { isVirtual, ExecutableType } from '../service'; @@ -179,13 +179,15 @@ export class ExecutableQuickPick implements vscode.Disposable { title: ' R executable file' }; void vscode.window.showOpenDialog(opts).then((epath) => { - const execPath = path.normalize(epath?.[0].fsPath); - if (execPath && validateRExecutablePath(execPath)) { - const rExec = this.service.executableFactory.create(execPath); - this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, rExec); - } else { - void vscode.window.showErrorMessage(ExecutableNotifications.badFolder); - this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, undefined); + if (epath) { + const execPath = path.normalize(epath?.[0].fsPath); + if (execPath && validateRExecutablePath(execPath)) { + const rExec = this.service.executableFactory.create(execPath); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, rExec); + } else { + void vscode.window.showErrorMessage(ExecutableNotifications.badFolder); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, undefined); + } } }); break; diff --git a/src/executables/ui/status.ts b/src/executables/ui/status.ts index 94d976009..30deeb41e 100644 --- a/src/executables/ui/status.ts +++ b/src/executables/ui/status.ts @@ -47,10 +47,18 @@ export class ExecutableStatusItem implements vscode.Disposable { } } - public async busy(prom: Promise): Promise { + public async busy(prom: Promise): Promise { this.languageStatusItem.busy = true; - await prom; - this.languageStatusItem.busy = false; + await prom.then((v: boolean) => { + if (!v) { + this.languageStatusItem.severity = vscode.LanguageStatusSeverity.Error; + } + this.languageStatusItem.busy = false; + }).catch(() => { + this.languageStatusItem.busy = false; + this.languageStatusItem.severity = vscode.LanguageStatusSeverity.Error; + this.languageStatusItem.detail = '$(error) Error activating virtual environment'; + }); } public dispose(): void { diff --git a/src/executables/ui/types.ts b/src/executables/ui/types.ts new file mode 100644 index 000000000..327981d71 --- /dev/null +++ b/src/executables/ui/types.ts @@ -0,0 +1,4 @@ +export enum ExecutableNotifications { + badFolder = 'Supplied R executable path is not a valid R path.', + badConfig = 'Configured path is not a valid R executable path.' +} \ No newline at end of file From b4bb2fa14f94461eedaf1546826b41629783f1a2 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Mon, 18 Jul 2022 14:01:23 +1000 Subject: [PATCH 11/22] Task rpath, path settings machine-overridable - rpath is machine-overridable - task rpath dependent on active workspace executable - quickpick shows arrow for active path --- package.json | 18 +++++--- src/executables/index.ts | 16 +++---- src/executables/service/class.ts | 8 +--- src/executables/service/index.ts | 2 +- src/executables/service/types.ts | 1 - src/executables/ui/quickpick.ts | 45 ++++++++++++-------- src/executables/ui/status.ts | 2 +- src/executables/{ => virtual}/conda.ts | 2 +- src/executables/virtual/index.ts | 2 + src/executables/{service => virtual}/renv.ts | 2 +- src/tasks.ts | 15 +++---- 11 files changed, 60 insertions(+), 53 deletions(-) rename src/executables/{ => virtual}/conda.ts (98%) create mode 100644 src/executables/virtual/index.ts rename src/executables/{service => virtual}/renv.ts (94%) diff --git a/package.json b/package.json index 07de3b608..df61a5677 100644 --- a/package.json +++ b/package.json @@ -1286,32 +1286,38 @@ "r.rpath.windows": { "type": "string", "default": "", - "description": "Path to an R executable for Windows. Must be \"vanilla\" R, not radian etc.!" + "description": "Path to an R executable for Windows. Must be \"vanilla\" R, not radian etc.!", + "scope": "machine-overridable" }, "r.rpath.mac": { "type": "string", "default": "", - "description": "Path to an R executable for macOS. Must be \"vanilla\" R, not radian etc.!" + "description": "Path to an R executable for macOS. Must be \"vanilla\" R, not radian etc.!", + "scope": "machine-overridable" }, "r.rpath.linux": { "type": "string", "default": "", - "description": "Path to an R executable for Linux. Must be \"vanilla\" R, not radian etc.!" + "description": "Path to an R executable for Linux. Must be \"vanilla\" R, not radian etc.!", + "scope": "machine-overridable" }, "r.rterm.windows": { "type": "string", "default": "", - "description": "R path for Windows. Can also be radian etc." + "description": "R path for Windows. Can also be radian etc.", + "scope": "machine-overridable" }, "r.rterm.mac": { "type": "string", "default": "", - "description": "R path for macOS. Can also be radian etc." + "description": "R path for macOS. Can also be radian etc.", + "scope": "machine-overridable" }, "r.rterm.linux": { "type": "string", "default": "", - "description": "R path for Linux. Can also be radian etc." + "description": "R path for Linux. Can also be radian etc.", + "scope": "machine-overridable" }, "r.rterm.option": { "type": "array", diff --git a/src/executables/index.ts b/src/executables/index.ts index aabd6ef6c..afbb419c4 100644 --- a/src/executables/index.ts +++ b/src/executables/index.ts @@ -5,12 +5,10 @@ import * as vscode from 'vscode'; import { ExecutableStatusItem, ExecutableQuickPick } from './ui'; import { isVirtual, RExecutableService, ExecutableType, WorkspaceExecutableEvent } from './service'; import { extensionContext } from '../extension'; -import { activateCondaEnvironment } from './conda'; - -export { ExecutableType as IRExecutable, VirtualExecutableType as IVirtualRExecutable } from './service'; +import { activateCondaEnvironment } from './virtual'; // super class that manages relevant sub classes -export class RExecutableManager implements vscode.Disposable { +export class RExecutableManager { private readonly executableService: RExecutableService; private statusBar: ExecutableStatusItem; private quickPick: ExecutableQuickPick; @@ -28,7 +26,9 @@ export class RExecutableManager implements vscode.Disposable { this.reload(); } }), - this + this.executableService, + this.statusBar, + this.quickPick ); this.reload(); } @@ -38,12 +38,6 @@ export class RExecutableManager implements vscode.Disposable { return new this(executableService); } - public dispose(): void { - this.executableService.dispose(); - this.statusBar.dispose(); - this.quickPick.dispose(); - } - public get executableQuickPick(): ExecutableQuickPick { return this.quickPick; } diff --git a/src/executables/service/class.ts b/src/executables/service/class.ts index 6648092a3..17f901d5e 100644 --- a/src/executables/service/class.ts +++ b/src/executables/service/class.ts @@ -1,7 +1,7 @@ -import { getCondaName, getRDetailsFromMetaHistory, isCondaInstallation } from '../conda'; +import { getCondaName, getRDetailsFromMetaHistory, isCondaInstallation } from '../virtual'; import { getRDetailsFromPath } from './locator'; import { RExecutableRegistry } from './registry'; -import { IExecutableDetails, ExecutableType } from './types'; +import { ExecutableType } from './types'; export function isVirtual(executable: AbstractExecutable): executable is VirtualRExecutable { return executable instanceof VirtualRExecutable; @@ -65,10 +65,6 @@ export class RExecutable extends AbstractExecutable { } return `$(error) R`; } - - protected getDetailsFromPath(execPath: string): IExecutableDetails { - return getRDetailsFromPath(execPath); - } } export class VirtualRExecutable extends AbstractExecutable { diff --git a/src/executables/service/index.ts b/src/executables/service/index.ts index 8e372a5c7..d98e07b9e 100644 --- a/src/executables/service/index.ts +++ b/src/executables/service/index.ts @@ -6,7 +6,7 @@ import { config, getCurrentWorkspaceFolder, getRPathConfigEntry } from '../../ut import { RExecutablePathStorage } from './pathStorage'; import { RExecutableRegistry } from './registry'; import { AbstractLocatorService, LocatorServiceFactory } from './locator'; -import { getRenvVersion } from './renv'; +import { getRenvVersion } from '../virtual'; import { ExecutableType, WorkspaceExecutableEvent } from './types'; export * from './types'; diff --git a/src/executables/service/types.ts b/src/executables/service/types.ts index 67aaa2368..b25f7d288 100644 --- a/src/executables/service/types.ts +++ b/src/executables/service/types.ts @@ -3,7 +3,6 @@ import * as vscode from 'vscode'; import { AbstractExecutable, VirtualRExecutable } from './class'; export type ExecutableType = AbstractExecutable; - export type VirtualExecutableType = VirtualRExecutable; export interface IExecutableDetails { diff --git a/src/executables/ui/quickpick.ts b/src/executables/ui/quickpick.ts index ad55088ef..4f81b52c3 100644 --- a/src/executables/ui/quickpick.ts +++ b/src/executables/ui/quickpick.ts @@ -6,7 +6,7 @@ import { validateRExecutablePath } from '..'; import { config, getCurrentWorkspaceFolder, getRPathConfigEntry, isMultiRoot } from '../../util'; import { isVirtual, ExecutableType } from '../service'; import { RExecutableService } from '../service'; -import { getRenvVersion } from '../service/renv'; +import { getRenvVersion } from '../virtual'; class ExecutableQuickPickItem implements vscode.QuickPickItem { public recommended: boolean; @@ -16,11 +16,13 @@ class ExecutableQuickPickItem implements vscode.QuickPickItem { public detail?: string; public picked?: boolean; public alwaysShow?: boolean; + public active: boolean; private _executable: ExecutableType; - constructor(executable: ExecutableType, recommended: boolean) { + constructor(executable: ExecutableType, service: RExecutableService, workspaceFolder: vscode.WorkspaceFolder, renvVersion?: string) { this._executable = executable; this.description = executable.rBin; + this.recommended = recommendPath(executable, workspaceFolder, renvVersion); if (isVirtual(executable)) { this.category = 'Virtual'; @@ -28,13 +30,17 @@ class ExecutableQuickPickItem implements vscode.QuickPickItem { this.category = 'Global'; } - this.recommended = recommended; - - if (recommended) { - this.label = `$(star) ${executable.tooltip}`; + if (this.recommended) { + this.label = `$(star-full) ${executable.tooltip}`; } else { this.label = executable.tooltip; } + + if (service.getWorkspaceExecutable(workspaceFolder?.uri?.fsPath)?.rBin === executable.rBin) { + this.label = `$(indent) ${this.label}`; + this.active = true; + } + } public get executable(): ExecutableType { @@ -44,7 +50,7 @@ class ExecutableQuickPickItem implements vscode.QuickPickItem { } enum PathQuickPickMenu { - search = '$(plus) Enter R binary path...', + search = '$(add) Enter R executable path...', configuration = '$(settings-gear) Configuration path' } @@ -105,18 +111,23 @@ export class ExecutableQuickPick implements vscode.Disposable { } ]; - [...this.service.executables].sort(sortExecutables).forEach((v) => { - const item = new ExecutableQuickPickItem(v, recommendPath(v, this.currentFolder, renvVersion)); - if (item.recommended) { - recommendedItems.push(item); + [...this.service.executables].sort(sortExecutables).forEach((executable) => { + const quickPickItem = new ExecutableQuickPickItem( + executable, + this.service, + this.currentFolder, + renvVersion + ); + if (quickPickItem.recommended) { + recommendedItems.push(quickPickItem); } else { - switch (item.category) { + switch (quickPickItem.category) { case 'Virtual': { - virtualItems.push(item); + virtualItems.push(quickPickItem); break; } case 'Global': { - globalItems.push(item); + globalItems.push(quickPickItem); break; } } @@ -125,9 +136,9 @@ export class ExecutableQuickPick implements vscode.Disposable { this.quickpick.items = [...qpItems, ...recommendedItems, ...virtualItems, ...globalItems]; - for (const item of this.quickpick.items) { - if (item.description === this.service.getWorkspaceExecutable(this.currentFolder?.uri?.fsPath)?.rBin) { - this.quickpick.activeItems = [item]; + for (const quickPickItem of this.quickpick.items) { + if ((quickPickItem as ExecutableQuickPickItem)?.active) { + this.quickpick.activeItems = [quickPickItem]; } } } diff --git a/src/executables/ui/status.ts b/src/executables/ui/status.ts index 30deeb41e..2c62eba17 100644 --- a/src/executables/ui/status.ts +++ b/src/executables/ui/status.ts @@ -13,7 +13,7 @@ export class ExecutableStatusItem implements vscode.Disposable { private languageStatusItem: vscode.LanguageStatusItem; private createItem(): vscode.LanguageStatusItem { - this.languageStatusItem = vscode.languages.createLanguageStatusItem('R Executable Selector', ['r', 'rmd']); + this.languageStatusItem = vscode.languages.createLanguageStatusItem('R Executable Selector', ['r', 'rmd', 'rProfile']); this.languageStatusItem.name = 'R Language Service'; this.languageStatusItem.command = { 'title': 'Select R executable', diff --git a/src/executables/conda.ts b/src/executables/virtual/conda.ts similarity index 98% rename from src/executables/conda.ts rename to src/executables/virtual/conda.ts index 715b4827e..f3a810451 100644 --- a/src/executables/conda.ts +++ b/src/executables/virtual/conda.ts @@ -1,5 +1,5 @@ -import { IExecutableDetails } from './service'; +import { IExecutableDetails } from '../service'; import * as fs from 'fs-extra'; import * as vscode from 'vscode'; import path = require('path'); diff --git a/src/executables/virtual/index.ts b/src/executables/virtual/index.ts new file mode 100644 index 000000000..5a8d63f61 --- /dev/null +++ b/src/executables/virtual/index.ts @@ -0,0 +1,2 @@ +export * from './conda'; +export * from './renv'; \ No newline at end of file diff --git a/src/executables/service/renv.ts b/src/executables/virtual/renv.ts similarity index 94% rename from src/executables/service/renv.ts rename to src/executables/virtual/renv.ts index 515b89800..a52c00a37 100644 --- a/src/executables/service/renv.ts +++ b/src/executables/virtual/renv.ts @@ -1,5 +1,5 @@ import path = require('path'); -import { IRenvLock } from './types'; +import { IRenvLock } from '../service/types'; import * as fs from 'fs-extra'; export function getRenvVersion(workspacePath: string): string | undefined { diff --git a/src/tasks.ts b/src/tasks.ts index c839ecdb2..ac541a11f 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; -import { getRpath } from './util'; +import { rExecService } from './extension'; const TYPE = 'R'; @@ -96,7 +96,7 @@ const rtasks: RTaskInfo[] = [ } ]; -function asRTask(rPath: string, folder: vscode.WorkspaceFolder | vscode.TaskScope, info: RTaskInfo): vscode.Task { +function asRTask(folder: vscode.WorkspaceFolder | vscode.TaskScope, info: RTaskInfo): vscode.Task { const args = makeRArgs(info.definition.options ?? defaultOptions, info.definition.code); const rtask: vscode.Task = new vscode.Task( info.definition, @@ -104,7 +104,7 @@ function asRTask(rPath: string, folder: vscode.WorkspaceFolder | vscode.TaskScop info.name, info.definition.type, new vscode.ProcessExecution( - rPath, + rExecService.activeExecutablePath, args, { cwd: info.definition.cwd, @@ -122,7 +122,7 @@ export class RTaskProvider implements vscode.TaskProvider { public type = TYPE; - public async provideTasks(): Promise { + public provideTasks(): vscode.Task[] { const folders = vscode.workspace.workspaceFolders; if (!folders) { @@ -130,13 +130,12 @@ export class RTaskProvider implements vscode.TaskProvider { } const tasks: vscode.Task[] = []; - const rPath = await getRpath(false); for (const folder of folders) { const isRPackage = fs.existsSync(path.join(folder.uri.fsPath, 'DESCRIPTION')); if (isRPackage) { for (const rtask of rtasks) { - const task = asRTask(rPath, folder, rtask); + const task = asRTask(folder, rtask); tasks.push(task); } } @@ -144,13 +143,13 @@ export class RTaskProvider implements vscode.TaskProvider { return tasks; } + // eslint-disable-next-line @typescript-eslint/require-await public async resolveTask(task: vscode.Task): Promise { const taskInfo: RTaskInfo = { definition: task.definition, group: task.group, name: task.name }; - const rPath = await getRpath(false); - return asRTask(rPath, vscode.TaskScope.Workspace, taskInfo); + return asRTask(vscode.TaskScope.Workspace, taskInfo); } } From 8f49eb42999a17200983b53b547f14681fc0d7f4 Mon Sep 17 00:00:00 2001 From: ElianHugh Date: Tue, 19 Jul 2022 21:15:42 +1000 Subject: [PATCH 12/22] Show error when selecting a bad installation Error is triggered when selecting an R installation that cannot be opened --- src/executables/index.ts | 5 ++--- src/executables/service/locator/unix.ts | 2 +- src/executables/service/locator/windows.ts | 6 +++--- src/executables/ui/quickpick.ts | 23 +++++++++++++--------- src/executables/ui/types.ts | 3 ++- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/executables/index.ts b/src/executables/index.ts index afbb419c4..28f1bd641 100644 --- a/src/executables/index.ts +++ b/src/executables/index.ts @@ -27,8 +27,7 @@ export class RExecutableManager { } }), this.executableService, - this.statusBar, - this.quickPick + this.statusBar ); this.reload(); } @@ -95,7 +94,7 @@ export class RExecutableManager { export function validateRExecutablePath(execPath: string): boolean { try { const basename = process.platform === 'win32' ? 'R.exe' : 'R'; - fs.accessSync(execPath, fs.constants.X_OK); + fs.accessSync(execPath, fs.constants.X_OK && fs.constants.R_OK); return (path.basename(execPath) === basename); } catch (error) { return false; diff --git a/src/executables/service/locator/unix.ts b/src/executables/service/locator/unix.ts index ec4c0c018..17472a058 100644 --- a/src/executables/service/locator/unix.ts +++ b/src/executables/service/locator/unix.ts @@ -17,7 +17,7 @@ export class UnixExecLocator extends AbstractLocatorService { new Set([ ...this.getPathFromDirs(), ...this.getPathFromEnv(), - ... this.getPathFromConda() + ...this.getPathFromConda() ]) )); this.emitter.fire(this._executablePaths); diff --git a/src/executables/service/locator/windows.ts b/src/executables/service/locator/windows.ts index 146d8cabc..8fb812685 100644 --- a/src/executables/service/locator/windows.ts +++ b/src/executables/service/locator/windows.ts @@ -4,7 +4,7 @@ import os = require('os'); import path = require('path'); import winreg = require('winreg'); import { getUniquePaths, AbstractLocatorService } from './shared'; -import { validateRExecutablePath } from '../..'; + const WindowsKnownPaths: string[] = []; @@ -88,11 +88,11 @@ export class WindowsExecLocator extends AbstractLocatorService { const i386 = `${rPath}\\${dir}\\bin\\i386\\R.exe`; const x64 = `${rPath}\\${dir}\\bin\\x64\\R.exe`; - if (validateRExecutablePath(i386)) { + if (fs.existsSync(i386)) { execPaths.push(i386); } - if (validateRExecutablePath(x64)) { + if (fs.existsSync(x64)) { execPaths.push(x64); } } diff --git a/src/executables/ui/quickpick.ts b/src/executables/ui/quickpick.ts index 4f81b52c3..15da96bf2 100644 --- a/src/executables/ui/quickpick.ts +++ b/src/executables/ui/quickpick.ts @@ -7,6 +7,7 @@ import { config, getCurrentWorkspaceFolder, getRPathConfigEntry, isMultiRoot } f import { isVirtual, ExecutableType } from '../service'; import { RExecutableService } from '../service'; import { getRenvVersion } from '../virtual'; +import { extensionContext } from '../../extension'; class ExecutableQuickPickItem implements vscode.QuickPickItem { public recommended: boolean; @@ -54,18 +55,16 @@ enum PathQuickPickMenu { configuration = '$(settings-gear) Configuration path' } -export class ExecutableQuickPick implements vscode.Disposable { + +export class ExecutableQuickPick { private readonly service: RExecutableService; - private quickpick: vscode.QuickPick; + private quickpick: vscode.QuickPick; private currentFolder: vscode.WorkspaceFolder; public constructor(service: RExecutableService) { this.service = service; this.currentFolder = getCurrentWorkspaceFolder(); - } - - public dispose(): void { - this.quickpick.dispose(); + extensionContext.subscriptions.push(this.quickpick); } private setItems(): void { @@ -178,7 +177,7 @@ export class ExecutableQuickPick implements vscode.Disposable { this.quickpick.hide(); } }); - this.quickpick.onDidChangeSelection((items: vscode.QuickPickItem[]) => { + this.quickpick.onDidChangeSelection((items: vscode.QuickPickItem[] | ExecutableQuickPickItem[]) => { const qpItem = items[0]; if (qpItem.label) { switch (qpItem.label) { @@ -189,7 +188,7 @@ export class ExecutableQuickPick implements vscode.Disposable { canSelectMany: false, title: ' R executable file' }; - void vscode.window.showOpenDialog(opts).then((epath) => { + void vscode.window.showOpenDialog(opts).then((epath: vscode.Uri[]) => { if (epath) { const execPath = path.normalize(epath?.[0].fsPath); if (execPath && validateRExecutablePath(execPath)) { @@ -215,7 +214,13 @@ export class ExecutableQuickPick implements vscode.Disposable { break; } default: { - this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, (qpItem as ExecutableQuickPickItem).executable); + const executable = (qpItem as ExecutableQuickPickItem).executable; + if (executable?.rVersion) { + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, executable); + } else { + void vscode.window.showErrorMessage(ExecutableNotifications.badInstallation); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, undefined); + } break; } } diff --git a/src/executables/ui/types.ts b/src/executables/ui/types.ts index 327981d71..f7489c788 100644 --- a/src/executables/ui/types.ts +++ b/src/executables/ui/types.ts @@ -1,4 +1,5 @@ export enum ExecutableNotifications { badFolder = 'Supplied R executable path is not a valid R path.', - badConfig = 'Configured path is not a valid R executable path.' + badConfig = 'Configured path is not a valid R executable path.', + badInstallation = 'Supplied R executable cannot be launched on this operating system.' } \ No newline at end of file From 60a6e7b34bc36a9472b993f29201bc8797de361d Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Mon, 25 Jul 2022 15:04:42 +1000 Subject: [PATCH 13/22] Inject conda values - Create R terminal now injects conda env values where appropriate - Conda env values are saved in /tmp/ for injection into processes --- src/executables/index.ts | 35 ++++++++++++++--- src/executables/service/class.ts | 4 +- src/executables/ui/status.ts | 7 +--- src/executables/virtual/conda.ts | 66 +++++++++++++++++++++----------- src/rTerminal.ts | 5 ++- src/util.ts | 3 +- 6 files changed, 83 insertions(+), 37 deletions(-) diff --git a/src/executables/index.ts b/src/executables/index.ts index 28f1bd641..dbeb9c977 100644 --- a/src/executables/index.ts +++ b/src/executables/index.ts @@ -1,6 +1,7 @@ import path = require('path'); import * as fs from 'fs-extra'; import * as vscode from 'vscode'; +import * as cp from 'child_process'; import { ExecutableStatusItem, ExecutableQuickPick } from './ui'; import { isVirtual, RExecutableService, ExecutableType, WorkspaceExecutableEvent } from './service'; @@ -73,17 +74,15 @@ export class RExecutableManager { void this.statusBar.busy(loading); } - - private async activateEnvironment(): Promise { + private async activateEnvironment(): Promise { if (!this.activeExecutable || !isVirtual(this.activeExecutable)) { - return Promise.resolve(true); + return Promise.resolve(); } - return activateCondaEnvironment(this.activeExecutable?.rBin); + await activateCondaEnvironment(this.activeExecutable); } } - /** * Is the folder of a given executable a valid R installation? * @@ -100,3 +99,29 @@ export function validateRExecutablePath(execPath: string): boolean { return false; } } + + +/** + * @description + * Takes an options object, and modifies the env values to allow for the injection + * of conda env values, and modify R binary paths for various rterms (e.g. radian) + * @export + * @template T + * @param {T} opts + * @param {ExecutableType} executable + * @returns {*} {T} + */ +export function modifyEnvVars(opts: T, executable: ExecutableType): T { + const envVars: Record = { + R_BINARY: executable.rBin + }; + const pathEnv: string = (opts?.env as Record)?.PATH ?? process.env?.PATH; + if (isVirtual(executable)) { + pathEnv ? + envVars['PATH'] = `${executable.envVar}:${pathEnv}` + : + envVars['PATH'] = executable.envVar; + } + opts['env'] = envVars; + return opts; +} \ No newline at end of file diff --git a/src/executables/service/class.ts b/src/executables/service/class.ts index 17f901d5e..627fd4af0 100644 --- a/src/executables/service/class.ts +++ b/src/executables/service/class.ts @@ -69,14 +69,15 @@ export class RExecutable extends AbstractExecutable { export class VirtualRExecutable extends AbstractExecutable { private _name: string; + public envVar: string; constructor(executablePath: string) { super(); this._name = getCondaName(executablePath); const details = getRDetailsFromMetaHistory(executablePath); - this._rBin = executablePath; this._rVersion = details?.version ?? ''; this._rArch = details?.arch ?? ''; + this._rBin = executablePath; } public get name(): string { @@ -89,4 +90,5 @@ export class VirtualRExecutable extends AbstractExecutable { } return `$(error) ${this.name}`; } + } diff --git a/src/executables/ui/status.ts b/src/executables/ui/status.ts index 2c62eba17..8d721ba8f 100644 --- a/src/executables/ui/status.ts +++ b/src/executables/ui/status.ts @@ -47,12 +47,9 @@ export class ExecutableStatusItem implements vscode.Disposable { } } - public async busy(prom: Promise): Promise { + public async busy(prom: Promise): Promise { this.languageStatusItem.busy = true; - await prom.then((v: boolean) => { - if (!v) { - this.languageStatusItem.severity = vscode.LanguageStatusSeverity.Error; - } + await prom.then(() => { this.languageStatusItem.busy = false; }).catch(() => { this.languageStatusItem.busy = false; diff --git a/src/executables/virtual/conda.ts b/src/executables/virtual/conda.ts index f3a810451..a3ba5d0b4 100644 --- a/src/executables/virtual/conda.ts +++ b/src/executables/virtual/conda.ts @@ -1,9 +1,11 @@ -import { IExecutableDetails } from '../service'; +import { IExecutableDetails, VirtualExecutableType } from '../service'; import * as fs from 'fs-extra'; import * as vscode from 'vscode'; +import * as cp from 'child_process'; import path = require('path'); -import { spawn } from 'child_process'; +import { exec } from 'child_process'; +import { rExecService, tmpDir } from '../../extension'; export function environmentIsActive(name: string): boolean { return process.env.CONDA_DEFAULT_ENV === name || @@ -32,7 +34,6 @@ export function getCondaActivationScript(executablePath: string): string { } export function isCondaInstallation(executablePath: string): boolean { - console.log(getCondaName(executablePath)); return fs.existsSync(getCondaMetaDir(executablePath)); } @@ -52,34 +53,55 @@ export function getRDetailsFromMetaHistory(executablePath: string): IExecutableD version: '' }; } +} +export function getActivationString(executablePath: string): string | undefined { + const activationPath = getCondaActivationScript(executablePath); + const commands = [ + activationPath, + `conda activate ${getCondaName(executablePath)}` + ].join(' & '); + return commands; } -export function activateCondaEnvironment(executablePath: string): Promise { +export function activateCondaEnvironment(executable: VirtualExecutableType): Promise { return new Promise((resolve, reject) => { try { - const opts = { - env: process.env, - shell: true - }; - const activationPath = (getCondaActivationScript(executablePath)); - const commands = [ - activationPath, - `conda activate ${getCondaName(executablePath)}` - ].join(' & '); - const childProc = spawn( - commands, - undefined, - opts - ); - childProc.on('exit', () => resolve(true)); + let command: string; + // need to fake activating conda environment by adding its env vars to the relevant R processes + if (process.platform === 'win32') { + // this assumes no powershell usage + // todo! need to check env saving for windows + command = [ + getCondaActivationScript(executable.rBin), + `conda activate ${executable.name}`, + `echo $PATH | awk -F':' '{ print $1}' > ${tmpDir()}/${executable.name}Env.txt` + ].join(' && '); + } else { + const unixCondaScript = path.join('/', 'etc', 'profile.d', 'conda.sh'); + command = [ + `source ${unixCondaScript}`, + `conda activate ${executable.name}`, + `echo $PATH | awk -F':' '{ print $1}' > ${tmpDir()}/${executable.name}Env.txt` + ].join(' &&'); + } + const childProc = exec(command); + childProc.on('error', (err) => { void vscode.window.showErrorMessage(`Error when activating conda environment: ${err.message}`); - reject(false); + reject(); + }); + childProc.on('exit', () => { + executable.envVar = readCondaBinFile(executable); + resolve(); }); } catch (error) { void vscode.window.showErrorMessage(`Error when activating conda environment: ${error as string}`); - reject(false); + reject(); } }); -} \ No newline at end of file +} + +function readCondaBinFile(executable: VirtualExecutableType) { + return fs.readFileSync(`${tmpDir()}/${executable.name}Env.txt`).toString().trim(); +} diff --git a/src/rTerminal.ts b/src/rTerminal.ts index daf268635..6ec8f331f 100644 --- a/src/rTerminal.ts +++ b/src/rTerminal.ts @@ -13,6 +13,7 @@ import { removeSessionFiles } from './session'; import { config, delay, getRterm } from './util'; import { rGuestService, isGuestSession } from './liveShare'; import * as fs from 'fs'; +import { modifyEnvVars } from './executables'; export let rTerm: vscode.Terminal; export async function runSource(echo: boolean): Promise { @@ -86,7 +87,7 @@ export async function runFromLineToEnd(): Promise { export async function makeTerminalOptions(): Promise { const termPath = await getRterm(); const shellArgs: string[] = config().get('rterm.option'); - const termOptions: vscode.TerminalOptions = { + let termOptions: vscode.TerminalOptions = { name: 'R Interactive', shellPath: termPath, shellArgs: shellArgs, @@ -101,7 +102,7 @@ export async function makeTerminalOptions(): Promise { VSCODE_WATCHER_DIR: homeExtDir() }; } - termOptions.env['R_BINARY'] = rExecService?.activeExecutable?.rBin; + termOptions = modifyEnvVars(termOptions, rExecService.activeExecutable); return termOptions; } diff --git a/src/util.ts b/src/util.ts index 92b985d66..f214e17dd 100644 --- a/src/util.ts +++ b/src/util.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import * as cp from 'child_process'; import { rGuestService, isGuestSession } from './liveShare'; -import { extensionContext } from './extension'; +import { extensionContext, rExecService } from './extension'; export function config(): vscode.WorkspaceConfiguration { return vscode.workspace.getConfiguration('r'); @@ -69,7 +69,6 @@ export function getRPathConfigEntry(term: boolean = false): string { export async function getRpath(quote = false, overwriteConfig?: string): Promise { let rpath = ''; - // try the config entry specified in the function arg: if (overwriteConfig) { rpath = config().get(overwriteConfig); From b4f3b4836c917f577b0aa988e728208280d87a1d Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Wed, 27 Jul 2022 15:28:59 +1000 Subject: [PATCH 14/22] Inject more env values, fix bug with reload --- package.json | 12 ++++++ src/executables/index.ts | 13 +++++-- src/executables/service/class.ts | 4 +- src/executables/ui/status.ts | 7 ++-- src/executables/virtual/conda.ts | 63 +++++++++++++++++++------------- 5 files changed, 64 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index df61a5677..c892c2a55 100644 --- a/package.json +++ b/package.json @@ -1283,6 +1283,18 @@ "type": "object", "title": "R", "properties": { + "r.executable.virtual.activateEnvironment": { + "type": "boolean", + "default": true, + "description": "Whether to automatically activate a conda environment containing a selected executable. *Recommended to keep enabled*", + "scope": "machine-overridable" + }, + "r.executable.virtual.condaPath": { + "type": "string", + "default": "", + "description": "Path to conda activation script. This will be `conda.sh` on unix or the `Activate` file on windows.", + "scope": "machine-overridable" + }, "r.rpath.windows": { "type": "string", "default": "", diff --git a/src/executables/index.ts b/src/executables/index.ts index dbeb9c977..7fd6126bb 100644 --- a/src/executables/index.ts +++ b/src/executables/index.ts @@ -6,7 +6,8 @@ import * as cp from 'child_process'; import { ExecutableStatusItem, ExecutableQuickPick } from './ui'; import { isVirtual, RExecutableService, ExecutableType, WorkspaceExecutableEvent } from './service'; import { extensionContext } from '../extension'; -import { activateCondaEnvironment } from './virtual'; +import { activateCondaEnvironment, condaPrefixPath } from './virtual'; +import { config } from '../util'; // super class that manages relevant sub classes export class RExecutableManager { @@ -75,7 +76,10 @@ export class RExecutableManager { } private async activateEnvironment(): Promise { - if (!this.activeExecutable || !isVirtual(this.activeExecutable)) { + if (!this.activeExecutable || + !isVirtual(this.activeExecutable) || + !!this.activeExecutable.envVar + ) { return Promise.resolve(); } await activateCondaEnvironment(this.activeExecutable); @@ -116,11 +120,14 @@ export function modifyEnvVars)?.PATH ?? process.env?.PATH; - if (isVirtual(executable)) { + if (isVirtual(executable) && executable.envVar) { pathEnv ? envVars['PATH'] = `${executable.envVar}:${pathEnv}` : envVars['PATH'] = executable.envVar; + envVars['CONDA_PREFIX'] = condaPrefixPath(executable.rBin); + envVars['CONDA_DEFAULT_ENV'] = executable.name ?? 'base'; + envVars['CONDA_PROMPT_MODIFIER'] = `(${envVars['CONDA_DEFAULT_ENV']})`; } opts['env'] = envVars; return opts; diff --git a/src/executables/service/class.ts b/src/executables/service/class.ts index 627fd4af0..b2a357262 100644 --- a/src/executables/service/class.ts +++ b/src/executables/service/class.ts @@ -1,4 +1,4 @@ -import { getCondaName, getRDetailsFromMetaHistory, isCondaInstallation } from '../virtual'; +import { condaName, getRDetailsFromMetaHistory, isCondaInstallation } from '../virtual'; import { getRDetailsFromPath } from './locator'; import { RExecutableRegistry } from './registry'; import { ExecutableType } from './types'; @@ -73,7 +73,7 @@ export class VirtualRExecutable extends AbstractExecutable { constructor(executablePath: string) { super(); - this._name = getCondaName(executablePath); + this._name = condaName(executablePath); const details = getRDetailsFromMetaHistory(executablePath); this._rVersion = details?.version ?? ''; this._rArch = details?.arch ?? ''; diff --git a/src/executables/ui/status.ts b/src/executables/ui/status.ts index 8d721ba8f..c694392c8 100644 --- a/src/executables/ui/status.ts +++ b/src/executables/ui/status.ts @@ -49,12 +49,11 @@ export class ExecutableStatusItem implements vscode.Disposable { public async busy(prom: Promise): Promise { this.languageStatusItem.busy = true; - await prom.then(() => { - this.languageStatusItem.busy = false; - }).catch(() => { - this.languageStatusItem.busy = false; + await prom.catch(() => { this.languageStatusItem.severity = vscode.LanguageStatusSeverity.Error; this.languageStatusItem.detail = '$(error) Error activating virtual environment'; + }).finally(() => { + this.languageStatusItem.busy = false; }); } diff --git a/src/executables/virtual/conda.ts b/src/executables/virtual/conda.ts index a3ba5d0b4..96123d226 100644 --- a/src/executables/virtual/conda.ts +++ b/src/executables/virtual/conda.ts @@ -6,17 +6,21 @@ import * as cp from 'child_process'; import path = require('path'); import { exec } from 'child_process'; import { rExecService, tmpDir } from '../../extension'; +import { config } from '../../util'; -export function environmentIsActive(name: string): boolean { - return process.env.CONDA_DEFAULT_ENV === name || - process.env.CONDA_PREFIX === name; +// Misc + +export function condaName(executablePath: string): string { + return path.basename(condaPrefixPath(executablePath)); } -export function getCondaName(executablePath: string): string { - return path.basename(path.dirname(getCondaMetaDir(executablePath))); +// Path functions + +export function condaPrefixPath(executablePath: string): string { + return path.dirname(condaMetaDirPath(executablePath)); } -export function getCondaMetaDir(executablePath: string): string { +export function condaMetaDirPath(executablePath: string): string { let envDir: string = executablePath; for (let index = 0; index < 4; index++) { envDir = path.dirname(envDir); @@ -24,24 +28,39 @@ export function getCondaMetaDir(executablePath: string): string { return path.join(envDir, 'conda-meta'); } -export function getCondaHistoryPath(executablePath: string): string { - return path.join(getCondaMetaDir(executablePath), 'history'); +export function condaHistoryPath(executablePath: string): string { + return path.join(condaMetaDirPath(executablePath), 'history'); +} + +export function condaActivationPath(executablePath: string): string { + const condaPathConfig = config().get('virtual.condaPath'); + if (condaPathConfig) { + return condaPathConfig; + } else if (process.platform === 'win32') { + const envDir = path.dirname(condaMetaDirPath(executablePath)); + return path.join(path.dirname(path.dirname(envDir)), 'Scripts', 'activate'); + } else { + return path.join('/', 'etc', 'profile.d', 'conda.sh'); + } } -export function getCondaActivationScript(executablePath: string): string { - const envDir = path.dirname(getCondaMetaDir(executablePath)); - return path.join(path.dirname(path.dirname(envDir)), 'Scripts', 'activate'); +// Bools + +export function environmentIsActive(executablePath: string): boolean { + return process.env.CONDA_DEFAULT_ENV === condaName(executablePath) || + process.env.CONDA_PREFIX === condaPrefixPath(executablePath); } export function isCondaInstallation(executablePath: string): boolean { - return fs.existsSync(getCondaMetaDir(executablePath)); + return fs.existsSync(condaMetaDirPath(executablePath)); } +// Extension + export function getRDetailsFromMetaHistory(executablePath: string): IExecutableDetails { try { - const reg = new RegExp(/([0-9]{2})::r-base-([0-9.]*)/g); - const historyContent = fs.readFileSync(getCondaHistoryPath(executablePath))?.toString(); + const historyContent = fs.readFileSync(condaHistoryPath(executablePath))?.toString(); const res = reg.exec(historyContent); return { arch: res?.[1] ? `${res[1]}-bit` : '', @@ -55,32 +74,24 @@ export function getRDetailsFromMetaHistory(executablePath: string): IExecutableD } } -export function getActivationString(executablePath: string): string | undefined { - const activationPath = getCondaActivationScript(executablePath); - const commands = [ - activationPath, - `conda activate ${getCondaName(executablePath)}` - ].join(' & '); - return commands; -} - export function activateCondaEnvironment(executable: VirtualExecutableType): Promise { return new Promise((resolve, reject) => { try { let command: string; // need to fake activating conda environment by adding its env vars to the relevant R processes + const activationPath = condaActivationPath(executable.rBin); + if (process.platform === 'win32') { // this assumes no powershell usage // todo! need to check env saving for windows command = [ - getCondaActivationScript(executable.rBin), + activationPath, `conda activate ${executable.name}`, `echo $PATH | awk -F':' '{ print $1}' > ${tmpDir()}/${executable.name}Env.txt` ].join(' && '); } else { - const unixCondaScript = path.join('/', 'etc', 'profile.d', 'conda.sh'); command = [ - `source ${unixCondaScript}`, + `source ${activationPath}`, `conda activate ${executable.name}`, `echo $PATH | awk -F':' '{ print $1}' > ${tmpDir()}/${executable.name}Env.txt` ].join(' &&'); From d247d4b4140c668ca1eeaa7515ddb25ab4dd15de Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Sun, 31 Jul 2022 10:04:27 +1000 Subject: [PATCH 15/22] Refactoring Minor stylistic changes, no behaviour change --- src/executables/index.ts | 1 - src/executables/service/index.ts | 10 +++++----- src/executables/service/types.ts | 7 ------- src/executables/ui/quickpick.ts | 26 +++++++++++++++----------- src/executables/ui/types.ts | 5 ----- src/executables/virtual/conda.ts | 13 ++++++------- src/executables/virtual/renv.ts | 6 +++--- src/executables/virtual/types.ts | 30 ++++++++++++++++++++++++++++++ 8 files changed, 59 insertions(+), 39 deletions(-) delete mode 100644 src/executables/ui/types.ts create mode 100644 src/executables/virtual/types.ts diff --git a/src/executables/index.ts b/src/executables/index.ts index 7fd6126bb..4161d0bd0 100644 --- a/src/executables/index.ts +++ b/src/executables/index.ts @@ -7,7 +7,6 @@ import { ExecutableStatusItem, ExecutableQuickPick } from './ui'; import { isVirtual, RExecutableService, ExecutableType, WorkspaceExecutableEvent } from './service'; import { extensionContext } from '../extension'; import { activateCondaEnvironment, condaPrefixPath } from './virtual'; -import { config } from '../util'; // super class that manages relevant sub classes export class RExecutableManager { diff --git a/src/executables/service/index.ts b/src/executables/service/index.ts index d98e07b9e..c8d5f8208 100644 --- a/src/executables/service/index.ts +++ b/src/executables/service/index.ts @@ -71,9 +71,9 @@ export class RExecutableService implements vscode.Disposable { public set activeExecutable(executable: ExecutableType | undefined) { if (executable === undefined) { this.workspaceExecutables.delete(getCurrentWorkspaceFolder().uri.fsPath); - this.executableStorage.setExecutablePath(getCurrentWorkspaceFolder().uri.fsPath, undefined); + this.executableStorage.setExecutablePath(getCurrentWorkspaceFolder().uri.fsPath, null); console.log('[RExecutableService] executable cleared'); - this.executableEmitter.fire(undefined); + this.executableEmitter.fire(null); } else if (this.activeExecutable !== executable) { this.workspaceExecutables.set(getCurrentWorkspaceFolder().uri.fsPath, executable); this.executableStorage.setExecutablePath(getCurrentWorkspaceFolder().uri.fsPath, executable.rBin); @@ -112,9 +112,9 @@ export class RExecutableService implements vscode.Disposable { */ public setWorkspaceExecutable(folder: string, executable: ExecutableType | undefined): void { if (this.workspaceExecutables.get(folder) !== executable) { - if (executable === undefined) { - this.executableStorage.setExecutablePath(folder, undefined); - this.workspaceEmitter.fire({ workingFolder: undefined, executable: executable }); + if (!executable) { + this.executableStorage.setExecutablePath(folder, null); + this.workspaceEmitter.fire({ workingFolder: null, executable: executable }); } else { const workspaceFolderUri = vscode.Uri.file(folder); this.workspaceEmitter.fire({ workingFolder: vscode.workspace.getWorkspaceFolder(workspaceFolderUri), executable: executable }); diff --git a/src/executables/service/types.ts b/src/executables/service/types.ts index b25f7d288..42fc72421 100644 --- a/src/executables/service/types.ts +++ b/src/executables/service/types.ts @@ -10,13 +10,6 @@ export interface IExecutableDetails { arch: string | undefined } -export interface IRenvLock { - 'R': { - 'Version': string, - 'Repositories': Record[] - }; -} - /** * @description * @export diff --git a/src/executables/ui/quickpick.ts b/src/executables/ui/quickpick.ts index 15da96bf2..7a88ae4f2 100644 --- a/src/executables/ui/quickpick.ts +++ b/src/executables/ui/quickpick.ts @@ -1,7 +1,6 @@ import path = require('path'); import * as vscode from 'vscode'; -import { ExecutableNotifications } from './types'; import { validateRExecutablePath } from '..'; import { config, getCurrentWorkspaceFolder, getRPathConfigEntry, isMultiRoot } from '../../util'; import { isVirtual, ExecutableType } from '../service'; @@ -9,6 +8,17 @@ import { RExecutableService } from '../service'; import { getRenvVersion } from '../virtual'; import { extensionContext } from '../../extension'; +enum ExecutableNotifications { + badFolder = 'Supplied R executable path is not a valid R path.', + badConfig = 'Configured path is not a valid R executable path.', + badInstallation = 'Supplied R executable cannot be launched on this operating system.' +} + +enum PathQuickPickMenu { + search = '$(add) Enter R executable path...', + configuration = '$(settings-gear) Configuration path' +} + class ExecutableQuickPickItem implements vscode.QuickPickItem { public recommended: boolean; public category: string; @@ -50,12 +60,6 @@ class ExecutableQuickPickItem implements vscode.QuickPickItem { } -enum PathQuickPickMenu { - search = '$(add) Enter R executable path...', - configuration = '$(settings-gear) Configuration path' -} - - export class ExecutableQuickPick { private readonly service: RExecutableService; private quickpick: vscode.QuickPick; @@ -173,7 +177,7 @@ export class ExecutableQuickPick { this.setItems(); this.quickpick.show(); } else { - this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, undefined); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, null); this.quickpick.hide(); } }); @@ -196,7 +200,7 @@ export class ExecutableQuickPick { this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, rExec); } else { void vscode.window.showErrorMessage(ExecutableNotifications.badFolder); - this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, undefined); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, null); } } }); @@ -209,7 +213,7 @@ export class ExecutableQuickPick { this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, rExec); } else { void vscode.window.showErrorMessage(ExecutableNotifications.badConfig); - this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, undefined); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, null); } break; } @@ -219,7 +223,7 @@ export class ExecutableQuickPick { this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, executable); } else { void vscode.window.showErrorMessage(ExecutableNotifications.badInstallation); - this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, undefined); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, null); } break; } diff --git a/src/executables/ui/types.ts b/src/executables/ui/types.ts deleted file mode 100644 index f7489c788..000000000 --- a/src/executables/ui/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum ExecutableNotifications { - badFolder = 'Supplied R executable path is not a valid R path.', - badConfig = 'Configured path is not a valid R executable path.', - badInstallation = 'Supplied R executable cannot be launched on this operating system.' -} \ No newline at end of file diff --git a/src/executables/virtual/conda.ts b/src/executables/virtual/conda.ts index 96123d226..14a6f856f 100644 --- a/src/executables/virtual/conda.ts +++ b/src/executables/virtual/conda.ts @@ -1,11 +1,10 @@ -import { IExecutableDetails, VirtualExecutableType } from '../service'; import * as fs from 'fs-extra'; import * as vscode from 'vscode'; -import * as cp from 'child_process'; import path = require('path'); +import { IExecutableDetails, VirtualExecutableType } from '../service'; import { exec } from 'child_process'; -import { rExecService, tmpDir } from '../../extension'; +import { tmpDir } from '../../extension'; import { config } from '../../util'; // Misc @@ -20,7 +19,7 @@ export function condaPrefixPath(executablePath: string): string { return path.dirname(condaMetaDirPath(executablePath)); } -export function condaMetaDirPath(executablePath: string): string { +function condaMetaDirPath(executablePath: string): string { let envDir: string = executablePath; for (let index = 0; index < 4; index++) { envDir = path.dirname(envDir); @@ -28,11 +27,11 @@ export function condaMetaDirPath(executablePath: string): string { return path.join(envDir, 'conda-meta'); } -export function condaHistoryPath(executablePath: string): string { +function condaHistoryPath(executablePath: string): string { return path.join(condaMetaDirPath(executablePath), 'history'); } -export function condaActivationPath(executablePath: string): string { +function condaActivationPath(executablePath: string): string { const condaPathConfig = config().get('virtual.condaPath'); if (condaPathConfig) { return condaPathConfig; @@ -40,7 +39,7 @@ export function condaActivationPath(executablePath: string): string { const envDir = path.dirname(condaMetaDirPath(executablePath)); return path.join(path.dirname(path.dirname(envDir)), 'Scripts', 'activate'); } else { - return path.join('/', 'etc', 'profile.d', 'conda.sh'); + return path.join('/', 'usr', 'bin', 'activate'); } } diff --git a/src/executables/virtual/renv.ts b/src/executables/virtual/renv.ts index a52c00a37..d99e5110b 100644 --- a/src/executables/virtual/renv.ts +++ b/src/executables/virtual/renv.ts @@ -1,5 +1,5 @@ import path = require('path'); -import { IRenvLock } from '../service/types'; +import { IRenvJSONLock } from '../virtual/types'; import * as fs from 'fs-extra'; export function getRenvVersion(workspacePath: string): string | undefined { @@ -9,7 +9,7 @@ export function getRenvVersion(workspacePath: string): string | undefined { if (!fs.existsSync(lockPath)) { return ''; } - const lockContent = fs.readJSONSync(lockPath) as IRenvLock; + const lockContent = fs.readJSONSync(lockPath) as IRenvJSONLock; return lockContent?.R?.Version ?? ''; } catch (error) { return ''; @@ -26,4 +26,4 @@ export function isRenvWorkspace(workspacePath: string): boolean { } catch (error) { return false; } -} \ No newline at end of file +} diff --git a/src/executables/virtual/types.ts b/src/executables/virtual/types.ts new file mode 100644 index 000000000..5a5311a17 --- /dev/null +++ b/src/executables/virtual/types.ts @@ -0,0 +1,30 @@ + +interface LockPackage { + Package: string, + Version: string, + Source: string, + Repository: string, + Hash?: string +} + +interface LockRepository { + 'Name': string, + 'URL': string +} + +type LockPythonType = 'virtualenv' | 'conda' | 'system' + +export interface IRenvJSONLock { + R: { + Version: string, + Repositories: LockRepository[] + }, + Packages: { + [key: string]: LockPackage + }, + Python?: { + Version: string, + Type: LockPythonType + Name?: string + } +} From ebf9821a1ec67840bdcb84e48672352831e72c2f Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Mon, 20 Feb 2023 16:38:02 +1100 Subject: [PATCH 16/22] Add tests --- src/executables/index.ts | 8 +- src/executables/service/class.ts | 8 +- src/executables/service/index.ts | 29 +++---- src/executables/service/locator/shared.ts | 4 +- src/executables/service/locator/windows.ts | 4 +- src/executables/service/pathStorage.ts | 7 +- src/executables/ui/quickpick.ts | 89 ++++++++++++---------- src/executables/ui/status.ts | 18 ++++- src/extension.ts | 2 +- src/test/suite/executables.test.ts | 67 ++++++++++++++++ src/util.ts | 13 ++++ 11 files changed, 182 insertions(+), 67 deletions(-) create mode 100644 src/test/suite/executables.test.ts diff --git a/src/executables/index.ts b/src/executables/index.ts index 4161d0bd0..4e455d4e1 100644 --- a/src/executables/index.ts +++ b/src/executables/index.ts @@ -22,7 +22,7 @@ export class RExecutableManager { this.onDidChangeActiveExecutable(() => { this.reload(); }), - vscode.window.onDidChangeActiveTextEditor((e: vscode.TextEditor) => { + vscode.window.onDidChangeActiveTextEditor((e: vscode.TextEditor | undefined) => { if (e?.document) { this.reload(); } @@ -42,6 +42,10 @@ export class RExecutableManager { return this.quickPick; } + public get languageStatusItem(): ExecutableStatusItem { + return this.statusBar; + } + public get activeExecutablePath(): string | undefined { return this.executableService.activeExecutable?.rBin; } @@ -71,7 +75,7 @@ export class RExecutableManager { public reload(): void { this.statusBar.refresh(); const loading = this.activateEnvironment(); - void this.statusBar.busy(loading); + void this.statusBar.makeBusy(loading); } private async activateEnvironment(): Promise { diff --git a/src/executables/service/class.ts b/src/executables/service/class.ts index b2a357262..58545cc88 100644 --- a/src/executables/service/class.ts +++ b/src/executables/service/class.ts @@ -32,9 +32,9 @@ export class RExecutableFactory { } export abstract class AbstractExecutable { - protected _rBin: string; - protected _rVersion: string; - protected _rArch: string; + protected _rBin!: string; + protected _rVersion!: string; + protected _rArch!: string; public get rBin(): string { return this._rBin; } @@ -69,7 +69,7 @@ export class RExecutable extends AbstractExecutable { export class VirtualRExecutable extends AbstractExecutable { private _name: string; - public envVar: string; + public envVar!: string; constructor(executablePath: string) { super(); diff --git a/src/executables/service/index.ts b/src/executables/service/index.ts index c8d5f8208..1f361b346 100644 --- a/src/executables/service/index.ts +++ b/src/executables/service/index.ts @@ -27,7 +27,7 @@ export class RExecutableService implements vscode.Disposable { private workspaceEmitter: vscode.EventEmitter; private workspaceExecutables: Map; - public readonly ready: Thenable; + public readonly ready!: Thenable; /** * Creates an instance of RExecutableService. @@ -69,16 +69,19 @@ export class RExecutableService implements vscode.Disposable { * @memberof RExecutableService */ public set activeExecutable(executable: ExecutableType | undefined) { - if (executable === undefined) { - this.workspaceExecutables.delete(getCurrentWorkspaceFolder().uri.fsPath); - this.executableStorage.setExecutablePath(getCurrentWorkspaceFolder().uri.fsPath, null); - console.log('[RExecutableService] executable cleared'); - this.executableEmitter.fire(null); - } else if (this.activeExecutable !== executable) { - this.workspaceExecutables.set(getCurrentWorkspaceFolder().uri.fsPath, executable); - this.executableStorage.setExecutablePath(getCurrentWorkspaceFolder().uri.fsPath, executable.rBin); - console.log('[RExecutableService] executable changed'); - this.executableEmitter.fire(executable); + const currentWorkspace = getCurrentWorkspaceFolder()?.uri?.fsPath; + if (currentWorkspace) { + if (executable === undefined) { + this.workspaceExecutables.delete(currentWorkspace); + this.executableStorage.setExecutablePath(currentWorkspace, undefined); + console.log('[RExecutableService] executable cleared'); + this.executableEmitter.fire(undefined); + } else if (this.activeExecutable !== executable) { + this.workspaceExecutables.set(currentWorkspace, executable); + this.executableStorage.setExecutablePath(currentWorkspace, executable.rBin); + console.log('[RExecutableService] executable changed'); + this.executableEmitter.fire(executable); + } } } @@ -113,8 +116,8 @@ export class RExecutableService implements vscode.Disposable { public setWorkspaceExecutable(folder: string, executable: ExecutableType | undefined): void { if (this.workspaceExecutables.get(folder) !== executable) { if (!executable) { - this.executableStorage.setExecutablePath(folder, null); - this.workspaceEmitter.fire({ workingFolder: null, executable: executable }); + this.executableStorage.setExecutablePath(folder, undefined); + this.workspaceEmitter.fire({ workingFolder: undefined, executable: executable }); } else { const workspaceFolderUri = vscode.Uri.file(folder); this.workspaceEmitter.fire({ workingFolder: vscode.workspace.getWorkspaceFolder(workspaceFolderUri), executable: executable }); diff --git a/src/executables/service/locator/shared.ts b/src/executables/service/locator/shared.ts index 19b8dd777..9c9d6b43c 100644 --- a/src/executables/service/locator/shared.ts +++ b/src/executables/service/locator/shared.ts @@ -59,8 +59,8 @@ export function getUniquePaths(paths: string[]): string[] { } export abstract class AbstractLocatorService { - protected _executablePaths: string[]; - protected emitter: vscode.EventEmitter; + protected _executablePaths!: string[]; + protected emitter!: vscode.EventEmitter; public abstract refreshPaths(): Promise; public get hasPaths(): boolean { return this._executablePaths.length > 0; diff --git a/src/executables/service/locator/windows.ts b/src/executables/service/locator/windows.ts index 8fb812685..2240118cc 100644 --- a/src/executables/service/locator/windows.ts +++ b/src/executables/service/locator/windows.ts @@ -59,9 +59,9 @@ export class WindowsExecLocator extends AbstractLocatorService { (c, e) => { reg.get('InstallPath', (err, result) => err === null ? c(result) : e(err)); } - ).then((item: winreg.RegistryItem) => { + ).then((item: unknown) => { if (item) { - const resolvedPath = item.value; + const resolvedPath = (item as winreg.RegistryItem).value; const i386 = `${resolvedPath}\\i386\\`; const x64 = `${resolvedPath}\\x64\\`; diff --git a/src/executables/service/pathStorage.ts b/src/executables/service/pathStorage.ts index 152e3b764..76f0f431c 100644 --- a/src/executables/service/pathStorage.ts +++ b/src/executables/service/pathStorage.ts @@ -23,7 +23,12 @@ export class RExecutablePathStorage { } public getActiveExecutablePath(): string | undefined { - return this.store.get(getCurrentWorkspaceFolder().uri.fsPath); + const currentWorkspace = getCurrentWorkspaceFolder()?.uri?.fsPath; + if (currentWorkspace) { + return this.store.get(currentWorkspace); + } else { + return undefined; + } } public getExecutablePath(workingDir: string): string | undefined { diff --git a/src/executables/ui/quickpick.ts b/src/executables/ui/quickpick.ts index 7a88ae4f2..101e28e76 100644 --- a/src/executables/ui/quickpick.ts +++ b/src/executables/ui/quickpick.ts @@ -27,7 +27,7 @@ class ExecutableQuickPickItem implements vscode.QuickPickItem { public detail?: string; public picked?: boolean; public alwaysShow?: boolean; - public active: boolean; + public active!: boolean; private _executable: ExecutableType; constructor(executable: ExecutableType, service: RExecutableService, workspaceFolder: vscode.WorkspaceFolder, renvVersion?: string) { @@ -62,8 +62,8 @@ class ExecutableQuickPickItem implements vscode.QuickPickItem { export class ExecutableQuickPick { private readonly service: RExecutableService; - private quickpick: vscode.QuickPick; - private currentFolder: vscode.WorkspaceFolder; + private quickpick!: vscode.QuickPick; + private currentFolder: vscode.WorkspaceFolder | undefined; public constructor(service: RExecutableService) { this.service = service; @@ -94,7 +94,7 @@ export class ExecutableQuickPick { }); } - const renvVersion = getRenvVersion(this.currentFolder.uri.fsPath) ?? undefined; + const renvVersion = this.currentFolder?.uri?.fsPath ? getRenvVersion(this.currentFolder?.uri?.fsPath) : undefined; const recommendedItems: vscode.QuickPickItem[] = [ { label: 'Recommended', @@ -115,23 +115,25 @@ export class ExecutableQuickPick { ]; [...this.service.executables].sort(sortExecutables).forEach((executable) => { - const quickPickItem = new ExecutableQuickPickItem( - executable, - this.service, - this.currentFolder, - renvVersion - ); - if (quickPickItem.recommended) { - recommendedItems.push(quickPickItem); - } else { - switch (quickPickItem.category) { - case 'Virtual': { - virtualItems.push(quickPickItem); - break; - } - case 'Global': { - globalItems.push(quickPickItem); - break; + if (this.currentFolder) { + const quickPickItem = new ExecutableQuickPickItem( + executable, + this.service, + this.currentFolder, + renvVersion + ); + if (quickPickItem.recommended) { + recommendedItems.push(quickPickItem); + } else { + switch (quickPickItem.category) { + case 'Virtual': { + virtualItems.push(quickPickItem); + break; + } + case 'Global': { + globalItems.push(quickPickItem); + break; + } } } } @@ -177,11 +179,13 @@ export class ExecutableQuickPick { this.setItems(); this.quickpick.show(); } else { - this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, null); + if (this.currentFolder) { + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, undefined); + } this.quickpick.hide(); } }); - this.quickpick.onDidChangeSelection((items: vscode.QuickPickItem[] | ExecutableQuickPickItem[]) => { + this.quickpick.onDidChangeSelection((items: readonly vscode.QuickPickItem[] | ExecutableQuickPickItem[]) => { const qpItem = items[0]; if (qpItem.label) { switch (qpItem.label) { @@ -192,15 +196,15 @@ export class ExecutableQuickPick { canSelectMany: false, title: ' R executable file' }; - void vscode.window.showOpenDialog(opts).then((epath: vscode.Uri[]) => { - if (epath) { + void vscode.window.showOpenDialog(opts).then((epath: vscode.Uri[] | undefined) => { + if (epath && this.currentFolder) { const execPath = path.normalize(epath?.[0].fsPath); if (execPath && validateRExecutablePath(execPath)) { const rExec = this.service.executableFactory.create(execPath); this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, rExec); } else { void vscode.window.showErrorMessage(ExecutableNotifications.badFolder); - this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, null); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, undefined); } } }); @@ -208,22 +212,26 @@ export class ExecutableQuickPick { } case PathQuickPickMenu.configuration: { const configPath = config().get(getRPathConfigEntry()); - if (configPath && validateRExecutablePath(configPath)) { - const rExec = this.service.executableFactory.create(configPath); - this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, rExec); - } else { - void vscode.window.showErrorMessage(ExecutableNotifications.badConfig); - this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, null); + if (this.currentFolder) { + if (configPath && validateRExecutablePath(configPath)) { + const rExec = this.service.executableFactory.create(configPath); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, rExec); + } else { + void vscode.window.showErrorMessage(ExecutableNotifications.badConfig); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, undefined); + } } break; } default: { const executable = (qpItem as ExecutableQuickPickItem).executable; - if (executable?.rVersion) { - this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, executable); - } else { - void vscode.window.showErrorMessage(ExecutableNotifications.badInstallation); - this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, null); + if (this.currentFolder) { + if (executable?.rVersion) { + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, executable); + } else { + void vscode.window.showErrorMessage(ExecutableNotifications.badInstallation); + this.service.setWorkspaceExecutable(this.currentFolder?.uri?.fsPath, undefined); + } } break; } @@ -237,9 +245,12 @@ export class ExecutableQuickPick { return await new Promise((res) => { setupQuickpickOpts(); setupQuickpickListeners(res); - void showWorkspaceFolderQP().then((folder: vscode.WorkspaceFolder) => { + void showWorkspaceFolderQP().then((folder: vscode.WorkspaceFolder | undefined) => { this.currentFolder = folder; - const currentExec = this.service.getWorkspaceExecutable(folder?.uri?.fsPath); + let currentExec; + if (this.currentFolder) { + currentExec = this.service.getWorkspaceExecutable(this.currentFolder?.uri?.fsPath); + } if (currentExec) { this.quickpick.placeholder = `Current path: ${currentExec.rBin}`; } else { diff --git a/src/executables/ui/status.ts b/src/executables/ui/status.ts index c694392c8..13e234818 100644 --- a/src/executables/ui/status.ts +++ b/src/executables/ui/status.ts @@ -5,12 +5,12 @@ import { RExecutableService } from '../service'; enum BinText { name = 'R Language Indicator', - missing = '$(warning) Select executable' + missing = '$(warning) Select R executable' } export class ExecutableStatusItem implements vscode.Disposable { private readonly service: RExecutableService; - private languageStatusItem: vscode.LanguageStatusItem; + private languageStatusItem!: vscode.LanguageStatusItem; private createItem(): vscode.LanguageStatusItem { this.languageStatusItem = vscode.languages.createLanguageStatusItem('R Executable Selector', ['r', 'rmd', 'rProfile']); @@ -28,6 +28,18 @@ export class ExecutableStatusItem implements vscode.Disposable { this.createItem(); } + public get text(): string { + return this.languageStatusItem.text; + } + + public get busy(): boolean { + return this.languageStatusItem.busy; + } + + public get severity(): vscode.LanguageStatusSeverity { + return this.languageStatusItem.severity; + } + public refresh(): void { const execState = this.service?.activeExecutable; if (execState) { @@ -47,7 +59,7 @@ export class ExecutableStatusItem implements vscode.Disposable { } } - public async busy(prom: Promise): Promise { + public async makeBusy(prom: Promise): Promise { this.languageStatusItem.busy = true; await prom.catch(() => { this.languageStatusItem.severity = vscode.LanguageStatusSeverity.Error; diff --git a/src/extension.ts b/src/extension.ts index c2a507acc..ae3777bbc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -65,7 +65,7 @@ export async function activate(context: vscode.ExtensionContext): Promise rExecService.executableQuickPick.showQuickPick(), + 'r.setExecutable': () => rExecService?.executableQuickPick.showQuickPick(), // run code from editor in terminal 'r.nrow': () => rTerminal.runSelectionOrWord(['nrow']), diff --git a/src/test/suite/executables.test.ts b/src/test/suite/executables.test.ts new file mode 100644 index 000000000..59f4ba888 --- /dev/null +++ b/src/test/suite/executables.test.ts @@ -0,0 +1,67 @@ +import vscode = require('vscode'); +import sinon = require('sinon'); +import path = require('path'); +import assert = require('assert'); + +import * as exec from '../../executables/service'; +import { ExecutableStatusItem } from '../../executables/ui'; +import { mockExtensionContext } from '../common'; + +const extension_root: string = path.join(__dirname, '..', '..', '..'); + +suite('Language status item', () => { + let sandbox: sinon.SinonSandbox; + setup(() => { + sandbox = sinon.createSandbox(); + }); + teardown(() => { + sandbox.restore(); + }); + + test('text', () => { + mockExtensionContext(extension_root, sandbox); + let executableValue: exec.ExecutableType | undefined = undefined; + const statusItem = new ExecutableStatusItem({ + get activeExecutable() { + return executableValue; + } + } as unknown as exec.RExecutableService); + assert.strictEqual( + statusItem.text, + '$(warning) Select R executable' + ); + + executableValue = { + get tooltip(): string { + return `R 4.0 64-bit`; + }, + rVersion: '4.0' + } as exec.ExecutableType; + statusItem.refresh(); + assert.strictEqual( + statusItem.text, + '4.0' + ); + statusItem.dispose(); + }); + + test('loading indicator', async () => { + mockExtensionContext(extension_root, sandbox); + const dummyPromise: Promise = new Promise(() => { + // + }); + const statusItem = new ExecutableStatusItem({ + get activeExecutable() { + return undefined; + } + } as unknown as exec.RExecutableService); + + void statusItem.makeBusy(dummyPromise); + assert.strictEqual(statusItem.busy, true); + + await statusItem.makeBusy(Promise.resolve()); + assert.strictEqual(statusItem.busy, false); + assert.strictEqual(statusItem.severity, vscode.LanguageStatusSeverity.Warning); + + }); +}); \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index ce4872b2e..8c51d503a 100644 --- a/src/util.ts +++ b/src/util.ts @@ -642,3 +642,16 @@ export function uniqueEntries(array: T[], isIdentical: (x: T, y: T) => boolea } return array.filter(uniqueFunction); } + +export function isMultiRoot(): boolean { + const folders = vscode?.workspace?.workspaceFolders; + if (folders) { + return folders.length > 1; + } else { + return false; + } +} + +export function normaliseRPathString(path: string): string { + return path.replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1'); +} \ No newline at end of file From 6ffd5caef6976875757f885b7cd71470b7315991 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Mon, 20 Feb 2023 18:41:24 +1100 Subject: [PATCH 17/22] Tests, rPath, linter fix Add tests for pathStorage, make rPath === activeExecPath again, linter --- src/cppProperties.ts | 2 +- src/executables/index.ts | 5 +++ src/executables/service/pathStorage.ts | 2 +- src/helpViewer/index.ts | 16 ++++---- src/languageService.ts | 2 +- src/rmarkdown/draft.ts | 2 +- src/rmarkdown/knit.ts | 2 +- src/rmarkdown/preview.ts | 2 +- src/tasks.ts | 8 ++-- src/test/suite/executables.test.ts | 51 +++++++++++++++++++++++++- src/util.ts | 25 +++---------- 11 files changed, 79 insertions(+), 38 deletions(-) diff --git a/src/cppProperties.ts b/src/cppProperties.ts index ef477ef1b..379977c3c 100644 --- a/src/cppProperties.ts +++ b/src/cppProperties.ts @@ -36,7 +36,7 @@ function platformChoose(win32: A, darwin: B, other: C): A | B | C { // See: https://code.visualstudio.com/docs/cpp/c-cpp-properties-schema-reference async function generateCppPropertiesProc(workspaceFolder: string) { - const rPath = await getRpath(); + const rPath = getRpath(); if (!rPath) { return; } diff --git a/src/executables/index.ts b/src/executables/index.ts index 4e455d4e1..cd0b68316 100644 --- a/src/executables/index.ts +++ b/src/executables/index.ts @@ -50,6 +50,11 @@ export class RExecutableManager { return this.executableService.activeExecutable?.rBin; } + /** + * Get the associated R executable for a given working directory path + * @param workingDir + * @returns + */ public getExecutablePath(workingDir: string): string | undefined { return this.executableService.getWorkspaceExecutable(workingDir)?.rBin; } diff --git a/src/executables/service/pathStorage.ts b/src/executables/service/pathStorage.ts index 76f0f431c..d7b2262ca 100644 --- a/src/executables/service/pathStorage.ts +++ b/src/executables/service/pathStorage.ts @@ -54,7 +54,7 @@ export class RExecutablePathStorage { private stringToMap(str: string): Map { try { - return new Map(JSON.parse(str)); + return new Map(JSON.parse(str) as Map); } catch (error) { return new Map(); } diff --git a/src/helpViewer/index.ts b/src/helpViewer/index.ts index 227e9cdde..e2e61b1d2 100644 --- a/src/helpViewer/index.ts +++ b/src/helpViewer/index.ts @@ -39,15 +39,15 @@ export const codeClickConfigDefault = { }; // Initialization function that is called once when activating the extension -export async function initializeHelp( +export function initializeHelp( context: vscode.ExtensionContext, rExtension: api.RExtension, -): Promise { +): RHelp | undefined { // set context value to indicate that the help related tree-view should be shown void vscode.commands.executeCommand('setContext', 'r.helpViewer.show', true); // get the "vanilla" R path from config - const rPath = await getRpath(); + const rPath = getRpath(); if(!rPath){ return undefined; } @@ -177,7 +177,7 @@ export interface HelpFile { // Internal representation of an "Alias" export interface Alias { - // main name of a help topic + // main name of a help topic name: string // one of possibly many aliases of the same help topic alias: string @@ -223,7 +223,7 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer ( a1.package === a2.package @@ -638,7 +638,7 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer { for (const previewer of this.previewProviders) { const ret = await previewer.getHelpFileFromRequestPath(requestPath); @@ -688,7 +688,7 @@ function pimpMyHelp(helpFile: HelpFile): HelpFile { helpFile.html = `
${html}
`; helpFile.isModified = true; } - + // parse the html string for futher modifications const $ = cheerio.load(helpFile.html); diff --git a/src/languageService.ts b/src/languageService.ts index 17a69a0b2..657de3102 100644 --- a/src/languageService.ts +++ b/src/languageService.ts @@ -53,7 +53,7 @@ export class LanguageService implements Disposable { let client: LanguageClient; const debug = config.get('lsp.debug'); - const rPath = await getRpath() || ''; // TODO: Abort gracefully + const rPath = getRpath() || ''; // TODO: Abort gracefully if (debug) { console.log(`R path: ${rPath}`); } diff --git a/src/rmarkdown/draft.ts b/src/rmarkdown/draft.ts index 172eb1b68..d30ff020e 100644 --- a/src/rmarkdown/draft.ts +++ b/src/rmarkdown/draft.ts @@ -20,7 +20,7 @@ interface TemplateItem extends QuickPickItem { async function getTemplateItems(cwd: string): Promise { const lim = '---vsc---'; - const rPath = await getRpath(); + const rPath = getRpath(); if (!rPath) { return undefined; } diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index 9225518c9..e0a1ca310 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -41,7 +41,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { if (!knitCommand) { return; } - this.rPath = await util.getRpath(); + this.rPath = util.getRpath(); const lim = '<<>>'; const re = new RegExp(`.*${lim}(.*)${lim}.*`, 'gms'); diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index eb5efb79b..512d68b72 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -307,7 +307,7 @@ export class RMarkdownPreviewManager extends RMarkdownManager { private async previewDocument(filePath: string, fileName?: string, viewer?: vscode.ViewColumn, currentViewColumn?: vscode.ViewColumn): Promise { const knitWorkingDir = this.getKnitDir(knitDir, filePath); const knitWorkingDirText = knitWorkingDir ? `${knitWorkingDir}` : ''; - this.rPath = await getRpath(); + this.rPath = getRpath(); const lim = '<<>>'; const re = new RegExp(`.*${lim}(.*)${lim}.*`, 'ms'); diff --git a/src/tasks.ts b/src/tasks.ts index fdb939139..994eac15d 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -122,7 +122,7 @@ export class RTaskProvider implements vscode.TaskProvider { public type = TYPE; - public async provideTasks(): Promise { + public provideTasks(): vscode.Task[] { const folders = vscode.workspace.workspaceFolders; if (!folders) { @@ -130,7 +130,7 @@ export class RTaskProvider implements vscode.TaskProvider { } const tasks: vscode.Task[] = []; - const rPath = await getRpath(false); + const rPath = getRpath(false); if (!rPath) { return []; } @@ -147,13 +147,13 @@ export class RTaskProvider implements vscode.TaskProvider { return tasks; } - public async resolveTask(task: vscode.Task): Promise { + public resolveTask(task: vscode.Task): vscode.Task { const taskInfo: RTaskInfo = { definition: task.definition, group: task.group, name: task.name }; - const rPath = await getRpath(false); + const rPath = getRpath(false); if (!rPath) { throw 'R path not set.'; } diff --git a/src/test/suite/executables.test.ts b/src/test/suite/executables.test.ts index 59f4ba888..593a036d9 100644 --- a/src/test/suite/executables.test.ts +++ b/src/test/suite/executables.test.ts @@ -3,13 +3,17 @@ import sinon = require('sinon'); import path = require('path'); import assert = require('assert'); + +import * as ext from '../../extension'; import * as exec from '../../executables/service'; import { ExecutableStatusItem } from '../../executables/ui'; import { mockExtensionContext } from '../common'; +import { RExecutablePathStorage } from '../../executables/service/pathStorage'; +import { DummyMemento } from '../../util'; const extension_root: string = path.join(__dirname, '..', '..', '..'); -suite('Language status item', () => { +suite('Language Status Item', () => { let sandbox: sinon.SinonSandbox; setup(() => { sandbox = sinon.createSandbox(); @@ -64,4 +68,49 @@ suite('Language status item', () => { assert.strictEqual(statusItem.severity, vscode.LanguageStatusSeverity.Warning); }); +}); + +suite('Executable Path Storage', () => { + let sandbox: sinon.SinonSandbox; + setup(() => { + sandbox = sinon.createSandbox(); + }); + teardown(() => { + sandbox.restore(); + }); + test('path storage + retrieval', () => { + const mockExtensionContext = { + environmentVariableCollection: sandbox.stub(), + extension: sandbox.stub(), + extensionMode: sandbox.stub(), + extensionPath: sandbox.stub(), + extensionUri: sandbox.stub(), + globalState: new DummyMemento(), + globalStorageUri: sandbox.stub(), + logUri: sandbox.stub(), + secrets: sandbox.stub(), + storageUri: sandbox.stub(), + subscriptions: [], + workspaceState: { + get: sinon.stub(), + update: sinon.stub() + }, + asAbsolutePath: (relativePath: string) => { + return path.join(extension_root, relativePath); + } + }; + sandbox.stub(ext, 'extensionContext').value(mockExtensionContext); + const pathStorage = new RExecutablePathStorage(); + pathStorage.setExecutablePath('/working/1', '/bin/1'); + assert.strictEqual( + pathStorage.getExecutablePath('/working/1'), + '/bin/1' + ); + + const pathStorage2 = new RExecutablePathStorage(); + assert.strictEqual( + pathStorage2.getExecutablePath('/working/1'), + '/bin/1' + ); + }); }); \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index 8c51d503a..ac6e06724 100644 --- a/src/util.ts +++ b/src/util.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import * as cp from 'child_process'; import { rGuestService, isGuestSession } from './liveShare'; -import { extensionContext } from './extension'; +import { extensionContext, rExecService } from './extension'; import { randomBytes } from 'crypto'; export function config(): vscode.WorkspaceConfiguration { @@ -68,27 +68,14 @@ export function getRPathConfigEntry(term: boolean = false): string { return `${trunc}.${platform}`; } -export async function getRpath(quote = false, overwriteConfig?: string): Promise { +export function getRpath(quote = false): string | undefined { let rpath: string | undefined = ''; - // try the config entry specified in the function arg: - if (overwriteConfig) { - rpath = config().get(overwriteConfig); - } - - // try the os-specific config entry for the rpath: - const configEntry = getRPathConfigEntry(); - rpath ||= config().get(configEntry); - - // read from path/registry: - rpath ||= await getRpathFromSystem(); - - // represent all invalid paths (undefined, '', null) as undefined: - rpath ||= undefined; + rpath = rExecService?.activeExecutablePath; if (!rpath) { // inform user about missing R path: - void vscode.window.showErrorMessage(`Cannot find R to use for help, package installation etc. Change setting r.${configEntry} to R path.`); + void vscode.window.showErrorMessage(`Cannot find R to use for help, package installation etc. Set executable path to R path.`); } else if (quote && /^[^'"].* .*[^'"]$/.exec(rpath)) { // if requested and rpath contains spaces, add quotes: rpath = `"${rpath}"`; @@ -308,7 +295,7 @@ export function getRLibPaths(): string | undefined { // Single quotes are ok. // export async function executeRCommand(rCommand: string, cwd?: string | URL, fallback?: string | ((e: Error) => string)): Promise { - const rPath = await getRpath(); + const rPath = getRpath(); if (!rPath) { return undefined; } @@ -518,7 +505,7 @@ export async function promptToInstallRPackage(name: string, section: string, cwd .then(async function (select) { if (select === 'Yes') { const repo = await getCranUrl('', cwd); - const rPath = await getRpath(); + const rPath = getRpath(); if (!rPath) { void vscode.window.showErrorMessage('R path not set', 'OK'); return; From 77669c4059cf34ea771645fcd2240bb70ad07b5f Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Mon, 20 Feb 2023 18:56:22 +1100 Subject: [PATCH 18/22] Conda settings --- package.json | 37 ++++++++++++++++++++++++++++++------- src/extension.ts | 2 +- src/tasks.ts | 1 + 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 761a37ae2..4bc747016 100644 --- a/package.json +++ b/package.json @@ -322,6 +322,11 @@ } ], "commands": [ + { + "category": "R", + "command": "r.setExecutable", + "title": "Select executable" + }, { "command": "r.workspaceViewer.refreshEntry", "title": "Manual Refresh", @@ -1317,35 +1322,53 @@ "type": "object", "title": "R", "properties": { + "r.executable.virtual.activateEnvironment": { + "type": "boolean", + "default": true, + "description": "Whether to automatically activate a conda environment containing a selected executable. *Recommended to keep enabled*", + "scope": "machine-overridable" + }, + "r.executable.virtual.condaPath": { + "type": "string", + "default": "", + "description": "Path to conda activation script. This will be `conda.sh` on unix or the `Activate` file on windows.", + "scope": "machine-overridable" + }, "r.rpath.windows": { "type": "string", "default": "", - "description": "Path to an R executable to launch R background processes (Windows). Must be \"vanilla\" R, not radian etc.!" + "description": "Path to an R executable to launch R background processes (Windows). Must be \"vanilla\" R, not radian etc.!", + "scope": "machine-overridable" }, "r.rpath.mac": { "type": "string", "default": "", - "description": "Path to an R executable to launch R background processes (macOS). Must be \"vanilla\" R, not radian etc.!" + "description": "Path to an R executable to launch R background processes (macOS). Must be \"vanilla\" R, not radian etc.!", + "scope": "machine-overridable" }, "r.rpath.linux": { "type": "string", "default": "", - "description": "Path to an R executable to launch R background processes (Linux). Must be \"vanilla\" R, not radian etc.!" + "description": "Path to an R executable to launch R background processes (Linux). Must be \"vanilla\" R, not radian etc.!", + "scope": "machine-overridable" }, "r.rterm.windows": { "type": "string", "default": "", - "description": "R path for interactive terminals (Windows). Can also be radian etc." + "description": "R path for interactive terminals (Windows). Can also be radian etc.", + "scope": "machine-overridable" }, "r.rterm.mac": { "type": "string", "default": "", - "description": "R path for interactive terminals (macOS). Can also be radian etc." + "description": "R path for interactive terminals (macOS). Can also be radian etc.", + "scope": "machine-overridable" }, "r.rterm.linux": { "type": "string", "default": "", - "description": "R path for interactive terminals (Linux). Can also be radian etc." + "description": "R path for interactive terminals (Linux). Can also be radian etc.", + "scope": "machine-overridable" }, "r.rterm.option": { "type": "array", @@ -2044,4 +2067,4 @@ "vsls": "^1.0.4753", "winreg": "^1.2.4" } -} +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index ae3777bbc..fcb8c54fa 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -202,7 +202,7 @@ export async function activate(context: vscode.ExtensionContext): Promise Date: Mon, 20 Feb 2023 21:56:11 +1100 Subject: [PATCH 19/22] rTerm uses executables terminals once again respect activeExecutable --- src/rTerminal.ts | 19 +++++++++++------- src/util.ts | 51 +++--------------------------------------------- 2 files changed, 15 insertions(+), 55 deletions(-) diff --git a/src/rTerminal.ts b/src/rTerminal.ts index ca8ad4966..4f1c64f06 100644 --- a/src/rTerminal.ts +++ b/src/rTerminal.ts @@ -6,6 +6,8 @@ import { isDeepStrictEqual } from 'util'; import * as vscode from 'vscode'; import { extensionContext, homeExtDir } from './extension'; +import { modifyEnvVars } from './executables'; +import { rExecService } from './extension'; import * as util from './util'; import * as selection from './selection'; import { getSelection } from './selection'; @@ -114,10 +116,10 @@ export async function runFromLineToEnd(): Promise { await runTextInTerm(text); } -export async function makeTerminalOptions(): Promise { - const termPath = await getRterm(); +export function makeTerminalOptions(): vscode.TerminalOptions { + const termPath = getRterm(); const shellArgs: string[] = config().get('rterm.option') || []; - const termOptions: vscode.TerminalOptions = { + let termOptions: vscode.TerminalOptions = { name: 'R Interactive', shellPath: termPath, shellArgs: shellArgs, @@ -132,11 +134,14 @@ export async function makeTerminalOptions(): Promise { VSCODE_WATCHER_DIR: homeExtDir() }; } + if (rExecService?.activeExecutable) { + termOptions = modifyEnvVars(termOptions, rExecService.activeExecutable); + } return termOptions; } -export async function createRTerm(preserveshow?: boolean): Promise { - const termOptions = await makeTerminalOptions(); +export function createRTerm(preserveshow?: boolean): boolean { + const termOptions = makeTerminalOptions(); const termPath = termOptions.shellPath; if(!termPath){ void vscode.window.showErrorMessage('Could not find R path. Please check r.term and r.path setting.'); @@ -150,11 +155,11 @@ export async function createRTerm(preserveshow?: boolean): Promise { return true; } -export async function restartRTerminal(): Promise{ +export function restartRTerminal(): void { if (typeof rTerm !== 'undefined'){ rTerm.dispose(); deleteTerminal(rTerm); - await createRTerm(true); + createRTerm(true); } } diff --git a/src/util.ts b/src/util.ts index ac6e06724..7a3085999 100644 --- a/src/util.ts +++ b/src/util.ts @@ -2,7 +2,6 @@ import { existsSync, PathLike, readFile } from 'fs-extra'; import * as fs from 'fs'; -import winreg = require('winreg'); import * as path from 'path'; import * as vscode from 'vscode'; import * as cp from 'child_process'; @@ -14,50 +13,6 @@ export function config(): vscode.WorkspaceConfiguration { return vscode.workspace.getConfiguration('r'); } -function getRfromEnvPath(platform: string) { - let splitChar = ':'; - let fileExtension = ''; - - if (platform === 'win32') { - splitChar = ';'; - fileExtension = '.exe'; - } - - const os_paths: string[] | string = process.env.PATH ? process.env.PATH.split(splitChar) : []; - for (const os_path of os_paths) { - const os_r_path: string = path.join(os_path, 'R' + fileExtension); - if (fs.existsSync(os_r_path)) { - return os_r_path; - } - } - return ''; -} - -export async function getRpathFromSystem(): Promise { - - let rpath = ''; - const platform: string = process.platform; - - rpath ||= getRfromEnvPath(platform); - - if ( !rpath && platform === 'win32') { - // Find path from registry - try { - const key = new winreg({ - hive: winreg.HKLM, - key: '\\Software\\R-Core\\R', - }); - const item: winreg.RegistryItem = await new Promise((c, e) => - key.get('InstallPath', (err, result) => err === null ? c(result) : e(err))); - rpath = path.join(item.value, 'bin', 'R.exe'); - } catch (e) { - rpath = ''; - } - } - - return rpath; -} - export function getRPathConfigEntry(term: boolean = false): string { const trunc = (term ? 'rterm' : 'rpath'); const platform = ( @@ -90,17 +45,17 @@ export function getRpath(quote = false): string | undefined { return rpath; } -export async function getRterm(): Promise { +export function getRterm(): string | undefined { const configEntry = getRPathConfigEntry(true); let rpath = config().get(configEntry); - rpath ||= await getRpathFromSystem(); + rpath ||= getRpath(); if (rpath !== '') { return rpath; } - void vscode.window.showErrorMessage(`Cannot find R for creating R terminal. Change setting r.${configEntry} to R path.`); + void vscode.window.showErrorMessage(`Cannot find R for creating R terminal. Set executable path to R path.`); return undefined; } From 25ed0c55f8ffc5eb293adcc465869b1ee81256c0 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Thu, 23 Feb 2023 22:15:05 +1100 Subject: [PATCH 20/22] Minor changes - Add documentation for various classes - Ensure that superfluous `await`s are removed - Rename some types for clarity --- src/executables/index.ts | 19 ++++++---- src/executables/service/class.ts | 18 +++++---- src/executables/service/index.ts | 25 +++++++------ src/executables/service/locator/index.ts | 10 ++++- src/executables/service/locator/shared.ts | 6 +++ src/executables/service/locator/unix.ts | 3 ++ src/executables/service/pathStorage.ts | 11 ++++++ src/executables/service/registry.ts | 16 ++++---- src/executables/service/types.ts | 8 ++-- src/executables/ui/quickpick.ts | 17 +++++---- src/executables/virtual/conda.ts | 6 +-- src/extension.ts | 45 ++++++++++++++--------- src/rTerminal.ts | 2 +- src/rstudioapi.ts | 4 +- src/test/suite/executables.test.ts | 4 +- 15 files changed, 121 insertions(+), 73 deletions(-) diff --git a/src/executables/index.ts b/src/executables/index.ts index cd0b68316..957e42418 100644 --- a/src/executables/index.ts +++ b/src/executables/index.ts @@ -4,7 +4,7 @@ import * as vscode from 'vscode'; import * as cp from 'child_process'; import { ExecutableStatusItem, ExecutableQuickPick } from './ui'; -import { isVirtual, RExecutableService, ExecutableType, WorkspaceExecutableEvent } from './service'; +import { isVirtual, RExecutableService, RExecutableType, WorkspaceExecutableEvent } from './service'; import { extensionContext } from '../extension'; import { activateCondaEnvironment, condaPrefixPath } from './virtual'; @@ -22,7 +22,7 @@ export class RExecutableManager { this.onDidChangeActiveExecutable(() => { this.reload(); }), - vscode.window.onDidChangeActiveTextEditor((e: vscode.TextEditor | undefined) => { + vscode.window.onDidChangeActiveTextEditor((e: vscode.TextEditor | undefined) => { if (e?.document) { this.reload(); } @@ -59,11 +59,11 @@ export class RExecutableManager { return this.executableService.getWorkspaceExecutable(workingDir)?.rBin; } - public get activeExecutable(): ExecutableType | undefined { + public get activeExecutable(): RExecutableType | undefined { return this.executableService.activeExecutable; } - public get onDidChangeActiveExecutable(): vscode.Event { + public get onDidChangeActiveExecutable(): vscode.Event { return this.executableService.onDidChangeActiveExecutable; } @@ -83,6 +83,11 @@ export class RExecutableManager { void this.statusBar.makeBusy(loading); } + /** + * Activates a Conda environment, but only if the currently active executable is virtual + * and has no obtained environmental variable . If determined that activation is not necessary, + * a resolved promise will be returned. + */ private async activateEnvironment(): Promise { if (!this.activeExecutable || !isVirtual(this.activeExecutable) || @@ -106,7 +111,7 @@ export function validateRExecutablePath(execPath: string): boolean { try { const basename = process.platform === 'win32' ? 'R.exe' : 'R'; fs.accessSync(execPath, fs.constants.X_OK && fs.constants.R_OK); - return (path.basename(execPath) === basename); + return (path.basename(execPath) === basename); } catch (error) { return false; } @@ -120,10 +125,10 @@ export function validateRExecutablePath(execPath: string): boolean { * @export * @template T * @param {T} opts - * @param {ExecutableType} executable + * @param {RExecutableType} executable * @returns {*} {T} */ -export function modifyEnvVars(opts: T, executable: ExecutableType): T { +export function modifyEnvVars(opts: T, executable: RExecutableType): T { const envVars: Record = { R_BINARY: executable.rBin }; diff --git a/src/executables/service/class.ts b/src/executables/service/class.ts index 58545cc88..4c6bfbb00 100644 --- a/src/executables/service/class.ts +++ b/src/executables/service/class.ts @@ -1,12 +1,16 @@ import { condaName, getRDetailsFromMetaHistory, isCondaInstallation } from '../virtual'; import { getRDetailsFromPath } from './locator'; import { RExecutableRegistry } from './registry'; -import { ExecutableType } from './types'; +import { RExecutableType } from './types'; -export function isVirtual(executable: AbstractExecutable): executable is VirtualRExecutable { +export function isVirtual(executable: AbstractRExecutable): executable is VirtualRExecutable { return executable instanceof VirtualRExecutable; } +/** + * Creates and caches instances of RExecutableType + * based on the provided executable path. + */ export class RExecutableFactory { private readonly registry: RExecutableRegistry; @@ -14,12 +18,12 @@ export class RExecutableFactory { this.registry = registry; } - public create(executablePath: string): ExecutableType { + public create(executablePath: string): RExecutableType { const cachedExec = [...this.registry.executables.values()].find((v) => v.rBin === executablePath); if (cachedExec) { return cachedExec; } else { - let executable: AbstractExecutable; + let executable: AbstractRExecutable; if (isCondaInstallation(executablePath)) { executable = new VirtualRExecutable(executablePath); } else { @@ -31,7 +35,7 @@ export class RExecutableFactory { } } -export abstract class AbstractExecutable { +export abstract class AbstractRExecutable { protected _rBin!: string; protected _rVersion!: string; protected _rArch!: string; @@ -50,7 +54,7 @@ export abstract class AbstractExecutable { } -export class RExecutable extends AbstractExecutable { +export class RExecutable extends AbstractRExecutable { constructor(executablePath: string) { super(); const details = getRDetailsFromPath(executablePath); @@ -67,7 +71,7 @@ export class RExecutable extends AbstractExecutable { } } -export class VirtualRExecutable extends AbstractExecutable { +export class VirtualRExecutable extends AbstractRExecutable { private _name: string; public envVar!: string; diff --git a/src/executables/service/index.ts b/src/executables/service/index.ts index 1f361b346..6e89d20bd 100644 --- a/src/executables/service/index.ts +++ b/src/executables/service/index.ts @@ -7,7 +7,7 @@ import { RExecutablePathStorage } from './pathStorage'; import { RExecutableRegistry } from './registry'; import { AbstractLocatorService, LocatorServiceFactory } from './locator'; import { getRenvVersion } from '../virtual'; -import { ExecutableType, WorkspaceExecutableEvent } from './types'; +import { RExecutableType, WorkspaceExecutableEvent } from './types'; export * from './types'; export * from './class'; @@ -23,9 +23,9 @@ export class RExecutableService implements vscode.Disposable { public executablePathLocator: AbstractLocatorService; private executableStorage: RExecutablePathStorage; private executableRegistry: RExecutableRegistry; - private executableEmitter: vscode.EventEmitter; + private executableEmitter: vscode.EventEmitter; private workspaceEmitter: vscode.EventEmitter; - private workspaceExecutables: Map; + private workspaceExecutables: Map; public readonly ready!: Thenable; @@ -38,8 +38,8 @@ export class RExecutableService implements vscode.Disposable { this.executableRegistry = new RExecutableRegistry(); this.executableStorage = new RExecutablePathStorage(); this.executableFactory = new RExecutableFactory(this.executableRegistry); - this.workspaceExecutables = new Map(); - this.executableEmitter = new vscode.EventEmitter(); + this.workspaceExecutables = new Map(); + this.executableEmitter = new vscode.EventEmitter(); this.workspaceEmitter = new vscode.EventEmitter(); this.executablePathLocator.executablePaths.forEach((path) => { this.executableFactory.create(path); @@ -56,11 +56,12 @@ export class RExecutableService implements vscode.Disposable { /** * @description + * Get a list of all registered executables * @readonly - * @type {Set} + * @type {Set} * @memberof RExecutableService */ - public get executables(): Set { + public get executables(): Set { return this.executableRegistry.executables; } @@ -68,7 +69,7 @@ export class RExecutableService implements vscode.Disposable { * @description * @memberof RExecutableService */ - public set activeExecutable(executable: ExecutableType | undefined) { + public set activeExecutable(executable: RExecutableType | undefined) { const currentWorkspace = getCurrentWorkspaceFolder()?.uri?.fsPath; if (currentWorkspace) { if (executable === undefined) { @@ -92,7 +93,7 @@ export class RExecutableService implements vscode.Disposable { * @type {RExecutable} * @memberof RExecutableService */ - public get activeExecutable(): ExecutableType | undefined { + public get activeExecutable(): RExecutableType | undefined { const currWorkspacePath = getCurrentWorkspaceFolder()?.uri?.fsPath; if (currWorkspacePath) { return this.workspaceExecutables.get(currWorkspacePath); @@ -113,7 +114,7 @@ export class RExecutableService implements vscode.Disposable { * @param {RExecutable} executable * @memberof RExecutableService */ - public setWorkspaceExecutable(folder: string, executable: ExecutableType | undefined): void { + public setWorkspaceExecutable(folder: string, executable: RExecutableType | undefined): void { if (this.workspaceExecutables.get(folder) !== executable) { if (!executable) { this.executableStorage.setExecutablePath(folder, undefined); @@ -135,7 +136,7 @@ export class RExecutableService implements vscode.Disposable { * @returns {*} {RExecutable} * @memberof RExecutableService */ - public getWorkspaceExecutable(folder: string): ExecutableType | undefined { + public getWorkspaceExecutable(folder: string): RExecutableType | undefined { return this.workspaceExecutables.get(folder); } @@ -147,7 +148,7 @@ export class RExecutableService implements vscode.Disposable { * @type {vscode.Event} * @memberof RExecutableService */ - public get onDidChangeActiveExecutable(): vscode.Event { + public get onDidChangeActiveExecutable(): vscode.Event { return this.executableEmitter.event; } diff --git a/src/executables/service/locator/index.ts b/src/executables/service/locator/index.ts index 68f3336d3..5e6091270 100644 --- a/src/executables/service/locator/index.ts +++ b/src/executables/service/locator/index.ts @@ -4,9 +4,15 @@ import { UnixExecLocator } from './unix'; import { WindowsExecLocator } from './windows'; import { AbstractLocatorService } from './shared'; - - +/** + * Static class factory for the creation of executable locators + */ export class LocatorServiceFactory { + /** + * Returns a new AbstractLocatorService, dependent on + * the process' platform + * @returns instance of AbstractLocatorService + */ static getLocator(): AbstractLocatorService { if (process.platform === 'win32') { return new WindowsExecLocator(); diff --git a/src/executables/service/locator/shared.ts b/src/executables/service/locator/shared.ts index 9c9d6b43c..2d5810d21 100644 --- a/src/executables/service/locator/shared.ts +++ b/src/executables/service/locator/shared.ts @@ -3,6 +3,12 @@ import * as fs from 'fs-extra'; import * as vscode from 'vscode'; import { normaliseRPathString } from '../../../util'; +/** + * Parses R version and architecture from a given R executable path. + * + * @param rPath string representing the path to an R executable. + * @returns object with R version and architecture as strings + */ export function getRDetailsFromPath(rPath: string): { version: string, arch: string } { try { const path = normaliseRPathString(rPath); diff --git a/src/executables/service/locator/unix.ts b/src/executables/service/locator/unix.ts index 17472a058..8205ae0c8 100644 --- a/src/executables/service/locator/unix.ts +++ b/src/executables/service/locator/unix.ts @@ -70,6 +70,9 @@ export class UnixExecLocator extends AbstractLocatorService { return execPaths; } + /** + * @returns Array of paths to R executables found in PATH variable + */ private getPathFromEnv(): string[] { const execPaths: string[] = []; const osPaths: string[] | string | undefined = process?.env?.PATH?.split(';'); diff --git a/src/executables/service/pathStorage.ts b/src/executables/service/pathStorage.ts index d7b2262ca..1c93d259a 100644 --- a/src/executables/service/pathStorage.ts +++ b/src/executables/service/pathStorage.ts @@ -2,6 +2,10 @@ import { extensionContext } from '../../extension'; import { getCurrentWorkspaceFolder } from '../../util'; +/** + * Stores and retrieves R executable paths for + * different workspace folders in vscode + */ export class RExecutablePathStorage { private store: Map; @@ -13,6 +17,13 @@ export class RExecutablePathStorage { return this.store; } + /** + * Sets the executable path for the given working directory. + * If binPath is undefined, it removes the executable path + * for the given working directory. + * @param workingDir + * @param binPath + */ public setExecutablePath(workingDir: string, binPath: string | undefined): void { if (binPath) { this.store.set(workingDir, binPath); diff --git a/src/executables/service/registry.ts b/src/executables/service/registry.ts index 9aa02bc4d..84434b720 100644 --- a/src/executables/service/registry.ts +++ b/src/executables/service/registry.ts @@ -1,31 +1,31 @@ -import { ExecutableType } from './types'; +import { RExecutableType } from './types'; // necessary to have an executable registry // so that we don't spam the (re)creation of executables export class RExecutableRegistry { - private readonly _executables: Set; + private readonly _executables: Set; constructor() { - this._executables = new Set(); + this._executables = new Set(); } - public get executables(): Set { + public get executables(): Set { return this._executables; } - public addExecutable(executable: ExecutableType): Set { + public addExecutable(executable: RExecutableType): Set { return this._executables.add(executable); } - public deleteExecutable(executable: ExecutableType): boolean { + public deleteExecutable(executable: RExecutableType): boolean { return this._executables.delete(executable); } - public hasExecutable(executable: ExecutableType): boolean { + public hasExecutable(executable: RExecutableType): boolean { return this._executables.has(executable); } - public getExecutablesWithVersion(version: string): ExecutableType[] { + public getExecutablesWithVersion(version: string): RExecutableType[] { return [...this._executables.values()].filter((v) => v.rVersion === version); } } diff --git a/src/executables/service/types.ts b/src/executables/service/types.ts index 42fc72421..2106f1e6b 100644 --- a/src/executables/service/types.ts +++ b/src/executables/service/types.ts @@ -1,9 +1,9 @@ import * as vscode from 'vscode'; -import { AbstractExecutable, VirtualRExecutable } from './class'; +import { AbstractRExecutable, VirtualRExecutable } from './class'; -export type ExecutableType = AbstractExecutable; -export type VirtualExecutableType = VirtualRExecutable; +export type RExecutableType = AbstractRExecutable; +export type VirtualRExecutableType = VirtualRExecutable; export interface IExecutableDetails { version: string | undefined, @@ -17,5 +17,5 @@ export interface IExecutableDetails { */ export interface WorkspaceExecutableEvent { workingFolder: vscode.WorkspaceFolder | undefined, - executable: ExecutableType | undefined + executable: RExecutableType | undefined } diff --git a/src/executables/ui/quickpick.ts b/src/executables/ui/quickpick.ts index 101e28e76..94d453b31 100644 --- a/src/executables/ui/quickpick.ts +++ b/src/executables/ui/quickpick.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import { validateRExecutablePath } from '..'; import { config, getCurrentWorkspaceFolder, getRPathConfigEntry, isMultiRoot } from '../../util'; -import { isVirtual, ExecutableType } from '../service'; +import { isVirtual, RExecutableType } from '../service'; import { RExecutableService } from '../service'; import { getRenvVersion } from '../virtual'; import { extensionContext } from '../../extension'; @@ -16,7 +16,8 @@ enum ExecutableNotifications { enum PathQuickPickMenu { search = '$(add) Enter R executable path...', - configuration = '$(settings-gear) Configuration path' + configuration = '$(settings-gear) Configuration path', + badPath = 'Invalid R path' } class ExecutableQuickPickItem implements vscode.QuickPickItem { @@ -28,9 +29,9 @@ class ExecutableQuickPickItem implements vscode.QuickPickItem { public picked?: boolean; public alwaysShow?: boolean; public active!: boolean; - private _executable: ExecutableType; + private _executable: RExecutableType; - constructor(executable: ExecutableType, service: RExecutableService, workspaceFolder: vscode.WorkspaceFolder, renvVersion?: string) { + constructor(executable: RExecutableType, service: RExecutableService, workspaceFolder: vscode.WorkspaceFolder, renvVersion?: string) { this._executable = executable; this.description = executable.rBin; this.recommended = recommendPath(executable, workspaceFolder, renvVersion); @@ -54,7 +55,7 @@ class ExecutableQuickPickItem implements vscode.QuickPickItem { } - public get executable(): ExecutableType { + public get executable(): RExecutableType { return this._executable; } @@ -74,7 +75,7 @@ export class ExecutableQuickPick { private setItems(): void { const qpItems: vscode.QuickPickItem[] = []; const configPath = config().get(getRPathConfigEntry()); - const sortExecutables = (a: ExecutableType, b: ExecutableType) => { + const sortExecutables = (a: RExecutableType, b: RExecutableType) => { return -a.rVersion.localeCompare(b.rVersion, undefined, { numeric: true, sensitivity: 'base' }); }; qpItems.push( @@ -89,7 +90,7 @@ export class ExecutableQuickPick { label: PathQuickPickMenu.configuration, alwaysShow: true, description: configPath, - detail: validateRExecutablePath(configPath) ? '' : 'Invalid R folder', + detail: validateRExecutablePath(configPath) ? '' : PathQuickPickMenu.badPath, picked: false }); } @@ -298,7 +299,7 @@ async function showWorkspaceFolderQP(): Promise { +export function activateCondaEnvironment(executable: VirtualRExecutableType): Promise { return new Promise((resolve, reject) => { try { let command: string; @@ -112,6 +112,6 @@ export function activateCondaEnvironment(executable: VirtualExecutableType): Pro }); } -function readCondaBinFile(executable: VirtualExecutableType) { +function readCondaBinFile(executable: VirtualRExecutableType) { return fs.readFileSync(`${tmpDir()}/${executable.name}Env.txt`).toString().trim(); } diff --git a/src/extension.ts b/src/extension.ts index fcb8c54fa..86900fbc9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -157,17 +157,18 @@ export async function activate(context: vscode.ExtensionContext): Promise('lsp.enabled')) { - const lsp = vscode.extensions.getExtension('reditorsupport.r-lsp'); - if (lsp) { - void vscode.window.showInformationMessage('The R language server extension has been integrated into vscode-R. You need to disable or uninstall REditorSupport.r-lsp and reload window to use the new version.'); - void vscode.commands.executeCommand('workbench.extensions.search', '@installed r-lsp'); - } else { - context.subscriptions.push(new languageService.LanguageService()); - } + if (rExecService.activeExecutable) { + activateServices(context, rExtension); } + // todo, this is a stopgap + rExecService?.onDidChangeActiveExecutable((exec) => { + if (exec) { + activateServices(context, rExtension); + } + }); + + // register on-enter rule for roxygen comments const wordPattern = /(-?\d*\.\d\w*)|([^`~!@$^&*()=+[{\]}\\|;:'",<>/\s]+)/g; vscode.languages.setLanguageConfiguration('r', { @@ -190,20 +191,14 @@ export async function activate(context: vscode.ExtensionContext): Promise('lsp.enabled')) { + const lsp = vscode.extensions.getExtension('reditorsupport.r-lsp'); + if (lsp) { + void vscode.window.showInformationMessage('The R language server extension has been integrated into vscode-R. You need to disable or uninstall REditorSupport.r-lsp and reload window to use the new version.'); + void vscode.commands.executeCommand('workbench.extensions.search', '@installed r-lsp'); + } else { + context.subscriptions.push(new languageService.LanguageService()); + } + } + // initialize httpgd viewer + globalHttpgdManager = httpgdViewer.initializeHttpgd(); + // initialize the package/help related functions + globalRHelp = rHelp.initializeHelp(context, rExtension); +} \ No newline at end of file diff --git a/src/rTerminal.ts b/src/rTerminal.ts index 4f1c64f06..425599885 100644 --- a/src/rTerminal.ts +++ b/src/rTerminal.ts @@ -228,7 +228,7 @@ export async function chooseTerminal(): Promise { } if (rTerm === undefined) { - await createRTerm(true); + createRTerm(true); await delay(200); // Let RTerm warm up } diff --git a/src/rstudioapi.ts b/src/rstudioapi.ts index ac4d738e5..77cba324e 100644 --- a/src/rstudioapi.ts +++ b/src/rstudioapi.ts @@ -75,7 +75,7 @@ export async function dispatchRStudioAPICall(action: string, args: any, sd: stri break; } case 'restart_r': { - await restartRTerminal(); + restartRTerminal(); await writeSuccessResponse(sd); break; } @@ -254,7 +254,7 @@ export function projectPath(): { path: string | undefined; } { } export async function documentNew(text: string, type: string, position: number[]): Promise { - const currentProjectPath = projectPath().path; + const currentProjectPath = projectPath().path; if (!currentProjectPath) { return; // TODO: Report failure } diff --git a/src/test/suite/executables.test.ts b/src/test/suite/executables.test.ts index 593a036d9..3cc5ab915 100644 --- a/src/test/suite/executables.test.ts +++ b/src/test/suite/executables.test.ts @@ -24,7 +24,7 @@ suite('Language Status Item', () => { test('text', () => { mockExtensionContext(extension_root, sandbox); - let executableValue: exec.ExecutableType | undefined = undefined; + let executableValue: exec.RExecutableType | undefined = undefined; const statusItem = new ExecutableStatusItem({ get activeExecutable() { return executableValue; @@ -40,7 +40,7 @@ suite('Language Status Item', () => { return `R 4.0 64-bit`; }, rVersion: '4.0' - } as exec.ExecutableType; + } as exec.RExecutableType; statusItem.refresh(); assert.strictEqual( statusItem.text, From 0366abe19505acdc5e6b452c6fe5ab42bf1d8971 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Fri, 24 Feb 2023 15:05:06 +1100 Subject: [PATCH 21/22] Basic executable locator test --- src/executables/virtual/renv.ts | 2 +- src/test/suite/executables.test.ts | 25 +++++++++++-------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/executables/virtual/renv.ts b/src/executables/virtual/renv.ts index d99e5110b..d64aaed29 100644 --- a/src/executables/virtual/renv.ts +++ b/src/executables/virtual/renv.ts @@ -1,6 +1,6 @@ import path = require('path'); -import { IRenvJSONLock } from '../virtual/types'; import * as fs from 'fs-extra'; +import { IRenvJSONLock } from '../virtual/types'; export function getRenvVersion(workspacePath: string): string | undefined { if (isRenvWorkspace(workspacePath)) { diff --git a/src/test/suite/executables.test.ts b/src/test/suite/executables.test.ts index 3cc5ab915..69a9b46c7 100644 --- a/src/test/suite/executables.test.ts +++ b/src/test/suite/executables.test.ts @@ -10,10 +10,11 @@ import { ExecutableStatusItem } from '../../executables/ui'; import { mockExtensionContext } from '../common'; import { RExecutablePathStorage } from '../../executables/service/pathStorage'; import { DummyMemento } from '../../util'; +import { LocatorServiceFactory } from '../../executables/service/locator'; const extension_root: string = path.join(__dirname, '..', '..', '..'); -suite('Language Status Item', () => { +suite('R Executable Service', () => { let sandbox: sinon.SinonSandbox; setup(() => { sandbox = sinon.createSandbox(); @@ -22,7 +23,7 @@ suite('Language Status Item', () => { sandbox.restore(); }); - test('text', () => { + test('status item text', () => { mockExtensionContext(extension_root, sandbox); let executableValue: exec.RExecutableType | undefined = undefined; const statusItem = new ExecutableStatusItem({ @@ -49,7 +50,7 @@ suite('Language Status Item', () => { statusItem.dispose(); }); - test('loading indicator', async () => { + test('status item loading indicator', async () => { mockExtensionContext(extension_root, sandbox); const dummyPromise: Promise = new Promise(() => { // @@ -66,18 +67,8 @@ suite('Language Status Item', () => { await statusItem.makeBusy(Promise.resolve()); assert.strictEqual(statusItem.busy, false); assert.strictEqual(statusItem.severity, vscode.LanguageStatusSeverity.Warning); - }); -}); -suite('Executable Path Storage', () => { - let sandbox: sinon.SinonSandbox; - setup(() => { - sandbox = sinon.createSandbox(); - }); - teardown(() => { - sandbox.restore(); - }); test('path storage + retrieval', () => { const mockExtensionContext = { environmentVariableCollection: sandbox.stub(), @@ -113,4 +104,10 @@ suite('Executable Path Storage', () => { '/bin/1' ); }); -}); \ No newline at end of file + + test('executable locator', async () => { + const locator = LocatorServiceFactory.getLocator(); + await locator.refreshPaths(); + assert.strictEqual(locator.executablePaths.length, 1); + }); +}); From 768a0060a0ad2800c8fc9ae2012210cac2d647cf Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Mon, 27 Feb 2023 19:55:22 +1100 Subject: [PATCH 22/22] No user-facing changes --- src/executables/index.ts | 2 +- src/executables/service/class.ts | 4 ++-- src/executables/ui/status.ts | 11 ++++++++++- src/test/suite/executables.test.ts | 24 ++++++------------------ 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/executables/index.ts b/src/executables/index.ts index 957e42418..a0a3bbbef 100644 --- a/src/executables/index.ts +++ b/src/executables/index.ts @@ -85,7 +85,7 @@ export class RExecutableManager { /** * Activates a Conda environment, but only if the currently active executable is virtual - * and has no obtained environmental variable . If determined that activation is not necessary, + * and has no obtained environmental variable. If determined that activation is not necessary, * a resolved promise will be returned. */ private async activateEnvironment(): Promise { diff --git a/src/executables/service/class.ts b/src/executables/service/class.ts index 4c6bfbb00..4c5de1fa2 100644 --- a/src/executables/service/class.ts +++ b/src/executables/service/class.ts @@ -90,9 +90,9 @@ export class VirtualRExecutable extends AbstractRExecutable { public get tooltip(): string { if (this.rVersion && this.rArch) { - return `${this.name} (R ${this.rVersion} ${this.rArch})`; + return `R ${this.rVersion} ${this.rArch} ('${this.name}')`; } - return `$(error) ${this.name}`; + return `$(error) '${this.name}'`; } } diff --git a/src/executables/ui/status.ts b/src/executables/ui/status.ts index 13e234818..233d6352e 100644 --- a/src/executables/ui/status.ts +++ b/src/executables/ui/status.ts @@ -8,12 +8,21 @@ enum BinText { missing = '$(warning) Select R executable' } +const rFileTypes = [ + 'r', + 'rmd', + 'rProfile', + 'rd', + 'rproj', + 'rnw' +]; + export class ExecutableStatusItem implements vscode.Disposable { private readonly service: RExecutableService; private languageStatusItem!: vscode.LanguageStatusItem; private createItem(): vscode.LanguageStatusItem { - this.languageStatusItem = vscode.languages.createLanguageStatusItem('R Executable Selector', ['r', 'rmd', 'rProfile']); + this.languageStatusItem = vscode.languages.createLanguageStatusItem('R Executable Selector', rFileTypes); this.languageStatusItem.name = 'R Language Service'; this.languageStatusItem.command = { 'title': 'Select R executable', diff --git a/src/test/suite/executables.test.ts b/src/test/suite/executables.test.ts index 69a9b46c7..2bf0a2a83 100644 --- a/src/test/suite/executables.test.ts +++ b/src/test/suite/executables.test.ts @@ -11,6 +11,7 @@ import { mockExtensionContext } from '../common'; import { RExecutablePathStorage } from '../../executables/service/pathStorage'; import { DummyMemento } from '../../util'; import { LocatorServiceFactory } from '../../executables/service/locator'; +import { VirtualRExecutable } from '../../executables/service'; const extension_root: string = path.join(__dirname, '..', '..', '..'); @@ -31,11 +32,7 @@ suite('R Executable Service', () => { return executableValue; } } as unknown as exec.RExecutableService); - assert.strictEqual( - statusItem.text, - '$(warning) Select R executable' - ); - + assert.strictEqual(statusItem.text, '$(warning) Select R executable'); executableValue = { get tooltip(): string { return `R 4.0 64-bit`; @@ -43,10 +40,7 @@ suite('R Executable Service', () => { rVersion: '4.0' } as exec.RExecutableType; statusItem.refresh(); - assert.strictEqual( - statusItem.text, - '4.0' - ); + assert.strictEqual(statusItem.text, '4.0'); statusItem.dispose(); }); @@ -93,21 +87,15 @@ suite('R Executable Service', () => { sandbox.stub(ext, 'extensionContext').value(mockExtensionContext); const pathStorage = new RExecutablePathStorage(); pathStorage.setExecutablePath('/working/1', '/bin/1'); - assert.strictEqual( - pathStorage.getExecutablePath('/working/1'), - '/bin/1' - ); + assert.strictEqual(pathStorage.getExecutablePath('/working/1'), '/bin/1'); const pathStorage2 = new RExecutablePathStorage(); - assert.strictEqual( - pathStorage2.getExecutablePath('/working/1'), - '/bin/1' - ); + assert.strictEqual(pathStorage2.getExecutablePath('/working/1'), '/bin/1'); }); test('executable locator', async () => { const locator = LocatorServiceFactory.getLocator(); await locator.refreshPaths(); - assert.strictEqual(locator.executablePaths.length, 1); + assert(locator.executablePaths.length > 0); }); });