Skip to content

Commit 05b11ee

Browse files
committed
R executable selection
1 parent 3af24dd commit 05b11ee

File tree

16 files changed

+872
-8
lines changed

16 files changed

+872
-8
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,11 @@
289289
}
290290
],
291291
"commands": [
292+
{
293+
"category": "R",
294+
"command": "r.setExecutable",
295+
"title": "Select executable"
296+
},
292297
{
293298
"command": "r.workspaceViewer.refreshEntry",
294299
"title": "Manual Refresh",

src/executables/executable.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { getVersionFromPath, getArchitectureFromPath } from './locator';
2+
3+
export class RExecutableFactory {
4+
static createExecutable(executablePath: string): RExecutable {
5+
if (new RegExp('\\.conda').exec(executablePath)?.length > 0) {
6+
return new VirtualRExecutable(executablePath);
7+
} else {
8+
return new RExecutable(executablePath);
9+
}
10+
}
11+
12+
private constructor() {
13+
//
14+
}
15+
}
16+
17+
export class RExecutable {
18+
private _rBin: string;
19+
private _rVersion: string;
20+
private _arch: string;
21+
22+
constructor(bin_path: string) {
23+
this._rBin = bin_path;
24+
this._rVersion = getVersionFromPath(bin_path);
25+
this._arch = getArchitectureFromPath(bin_path);
26+
}
27+
28+
public get rBin(): string {
29+
return this._rBin;
30+
}
31+
32+
public get rVersion(): string {
33+
return this._rVersion;
34+
}
35+
36+
public get rArch(): string {
37+
return this._arch;
38+
}
39+
40+
public get tooltip(): string {
41+
return `R ${this.rVersion} ${this.rArch}`;
42+
}
43+
}
44+
45+
export class VirtualRExecutable extends RExecutable {
46+
constructor(bin_path: string) {
47+
super(bin_path);
48+
}
49+
50+
public get name(): string {
51+
const reg = new RegExp('(?<=\\/envs\\/)(.*?)(?=\\/)');
52+
return reg.exec(this.rBin)[0];
53+
}
54+
55+
public get tooltip(): string {
56+
return `${this.name} (R ${this.rVersion} ${this.rArch})`;
57+
}
58+
59+
// todo, hardcoded
60+
public get activationCommand(): string[] {
61+
return ['activate', this.name];
62+
}
63+
}

src/executables/index.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import path = require('path');
2+
import * as fs from 'fs-extra';
3+
import * as vscode from 'vscode';
4+
5+
import { LocatorServiceFactory, AbstractLocatorService } from './locator';
6+
import { ExecutableStatusItem, ExecutableQuickPick } from './ui';
7+
import { RExecutableService, WorkspaceExecutableEvent } from './service';
8+
import { extensionContext } from '../extension';
9+
import { spawnAsync } from '../util';
10+
import { RExecutable, VirtualRExecutable } from './executable';
11+
12+
// super class that manages relevant sub classes
13+
export class RExecutableManager {
14+
private retrievalService: AbstractLocatorService;
15+
private statusBar: ExecutableStatusItem;
16+
private quickPick: ExecutableQuickPick;
17+
private executableService: RExecutableService;
18+
19+
constructor() {
20+
this.retrievalService = LocatorServiceFactory.getLocator();
21+
this.retrievalService.refreshPaths();
22+
this.executableService = new RExecutableService();
23+
this.statusBar = new ExecutableStatusItem(this.executableService);
24+
this.quickPick = new ExecutableQuickPick(this.executableService, this.retrievalService);
25+
26+
extensionContext.subscriptions.push(
27+
this.onDidChangeActiveExecutable(() => {
28+
this.reload();
29+
}),
30+
vscode.window.onDidChangeActiveTextEditor((e: vscode.TextEditor) => {
31+
if (e?.document) {
32+
this.reload();
33+
}
34+
}),
35+
this.executableService,
36+
this.statusBar,
37+
this.quickPick
38+
);
39+
this.reload();
40+
}
41+
42+
public get activeExecutable(): RExecutable {
43+
return this.executableService.activeExecutable;
44+
}
45+
46+
public get executableQuickPick(): ExecutableQuickPick {
47+
return this.quickPick;
48+
}
49+
50+
public get executableStatusItem(): ExecutableStatusItem {
51+
return this.statusBar;
52+
}
53+
54+
public get onDidChangeActiveExecutable(): vscode.Event<RExecutable> {
55+
return this.executableService.onDidChangeActiveExecutable;
56+
}
57+
58+
public get onDidChangeWorkspaceExecutable(): vscode.Event<WorkspaceExecutableEvent> {
59+
return this.executableService.onDidChangeWorkspaceExecutable;
60+
}
61+
62+
/**
63+
* @description
64+
* Orders a refresh of the executable manager, causing a refresh of the language status bar item and
65+
* activates a conda environment if present.
66+
* @memberof RExecutableManager
67+
*/
68+
public reload(): void {
69+
this.statusBar.refresh();
70+
const loading = this.activateEnvironment();
71+
void this.statusBar.busy(loading);
72+
}
73+
74+
private async activateEnvironment(): Promise<unknown> {
75+
const opts = {
76+
env: {
77+
...process.env
78+
},
79+
};
80+
if (this.activeExecutable instanceof VirtualRExecutable && opts.env.CONDA_DEFAULT_ENV !== this.activeExecutable.name) {
81+
return spawnAsync(
82+
'conda', // hard coded for now
83+
this.activeExecutable.activationCommand,
84+
opts,
85+
undefined
86+
);
87+
} else {
88+
return Promise.resolve();
89+
}
90+
}
91+
92+
}
93+
94+
95+
/**
96+
* Is the folder of a given executable a valid R installation?
97+
*
98+
* A path is valid if the folder contains the R executable and an Rcmd file.
99+
* @param execPath
100+
* @returns boolean
101+
*/
102+
export function validateRFolder(execPath: string): boolean {
103+
const basename = process.platform === 'win32' ? 'R.exe' : 'R';
104+
const scriptPath = path.normalize(`${execPath}/../Rcmd`);
105+
return fs.existsSync(execPath) && path.basename(basename) && fs.existsSync(scriptPath);
106+
}

src/executables/locator/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export * from './shared';
2+
3+
import { UnixExecLocator } from './unix';
4+
import { WindowsExecLocator } from './windows';
5+
import { AbstractLocatorService } from './shared';
6+
7+
export class LocatorServiceFactory {
8+
static getLocator(): AbstractLocatorService {
9+
if (process.platform === 'win32') {
10+
return new WindowsExecLocator();
11+
} else {
12+
return new UnixExecLocator();
13+
}
14+
}
15+
16+
private constructor() {
17+
//
18+
}
19+
}

src/executables/locator/shared.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import path = require('path');
2+
import * as fs from 'fs-extra';
3+
4+
export function getVersionFromPath(rPath: string): string {
5+
if (process.platform === 'win32') {
6+
// not sure how to do this
7+
return '';
8+
} else {
9+
try {
10+
const scriptPath = path.normalize(`${rPath}/../Rcmd`);
11+
const rCmdFile = fs.readFileSync(scriptPath, 'utf-8');
12+
const regex = /(?<=R_VERSION=)[0-9.]*/g;
13+
const version = regex.exec(rCmdFile)?.[0];
14+
return version ?? '';
15+
} catch (error) {
16+
return '';
17+
}
18+
}
19+
}
20+
21+
export function getArchitectureFromPath(path: string): string {
22+
if (process.platform === 'win32') {
23+
// \\bin\\i386 = 32bit
24+
// \\bin\\x64 = 64bit
25+
return '';
26+
} else {
27+
return '64-bit';
28+
}
29+
}
30+
31+
export abstract class AbstractLocatorService {
32+
protected binary_paths: string[];
33+
public abstract get hasBinaries(): boolean;
34+
public abstract get binaries(): string[];
35+
public abstract refreshPaths(): void;
36+
}

src/executables/locator/unix.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import * as fs from 'fs-extra';
2+
import * as os from 'os';
3+
import path = require('path');
4+
5+
import { AbstractLocatorService } from './shared';
6+
7+
export class UnixExecLocator extends AbstractLocatorService {
8+
public get hasBinaries(): boolean {
9+
return this.binary_paths.length > 0;
10+
}
11+
public get binaries(): string[] {
12+
return this.binary_paths;
13+
}
14+
public refreshPaths(): void {
15+
this.binary_paths = Array.from(
16+
new Set([
17+
...this.getHomeFromDirs(),
18+
...this.getHomeFromEnv(),
19+
... this.getHomeFromConda()
20+
])
21+
);
22+
}
23+
24+
private potential_bin_paths: string[] = [
25+
'/usr/lib64/R/bin/R',
26+
'/usr/lib/R/bin/R',
27+
'/usr/local/lib64/R/bin/R',
28+
'/usr/local/lib/R/bin/R',
29+
'/opt/local/lib64/R/bin/R',
30+
'/opt/local/lib/R/bin/R'
31+
];
32+
33+
private getHomeFromDirs(): string[] {
34+
const dirBins: string[] = [];
35+
for (const bin of this.potential_bin_paths) {
36+
if (fs.existsSync(bin)) {
37+
dirBins.push(bin);
38+
}
39+
}
40+
return dirBins;
41+
}
42+
43+
private getHomeFromConda(): string[] {
44+
const dirBins: string[] = [];
45+
const conda_dirs = [
46+
`${os.homedir()}/.conda/environments.txt`
47+
];
48+
for (const dir of conda_dirs) {
49+
if (fs.existsSync(dir)) {
50+
const lines = fs.readFileSync(dir).toString();
51+
for (const line of lines.split('\n')) {
52+
if (line) {
53+
const potential_dirs = [
54+
`${line}/lib64/R/bin/R`,
55+
`${line}/lib/R/bin/R`
56+
];
57+
for (const dir of potential_dirs) {
58+
if (fs.existsSync(dir)) {
59+
dirBins.push(dir);
60+
}
61+
}
62+
}
63+
}
64+
}
65+
}
66+
return dirBins;
67+
}
68+
69+
private getHomeFromEnv(): string[] {
70+
const envBins: string[] = [];
71+
const os_paths: string[] | string = process.env.PATH.split(';');
72+
73+
for (const os_path of os_paths) {
74+
const os_r_path: string = path.join(os_path, 'R');
75+
if (fs.existsSync(os_r_path)) {
76+
envBins.push(os_r_path);
77+
}
78+
}
79+
return envBins;
80+
}
81+
}

0 commit comments

Comments
 (0)