Skip to content

Commit e4ff4df

Browse files
matheuscumpianrafaelfranca
authored andcommitted
add support for rv version manager
1 parent 5162d40 commit e4ff4df

File tree

7 files changed

+204
-3
lines changed

7 files changed

+204
-3
lines changed

jekyll/version-managers.markdown

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ Ensure Mise is up-to-date: https://mise.jdx.dev/faq.html#mise-is-failing-or-not-
3131

3232
Ensure RVM is up-to-date: https://rvm.io/rvm/upgrading
3333

34+
## rv
35+
36+
Ensure rv 0.3.1 or later is installed. The extension automatically detects the Ruby version from a `.ruby-version` or `.tool-versions` file in the project.
37+
38+
Learn more about rv: https://github.com/spinel-coop/rv
39+
3440
## Custom activation
3541

3642
If you're using a different version manager that's not supported by this extension or if you're manually inserting the Ruby

project-words

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ libexec
5555
linearization
5656
Linearizes
5757
linearizing
58+
linuxbrew
5859
lockfiles
5960
lspconfig
6061
Lstart
@@ -74,8 +75,8 @@ qtlzwssomeking
7475
quickfixes
7576
quux
7677
quxx
77-
rdbg
7878
rbundler
79+
rdbg
7980
readlines
8081
realpath
8182
reparsing
@@ -110,8 +111,8 @@ testrunner
110111
testrunnermediator
111112
truffleruby
112113
tsort
113-
unaliased
114114
ucrt
115+
unaliased
115116
unindexed
116117
unparser
117118
unresolve

vscode/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ by clicking `Change version manager` in the language status center or by changin
105105
// "asdf"
106106
// "chruby"
107107
// "rbenv"
108+
// "rv"
108109
// "rvm"
109110
// "shadowenv"
110111
// "mise"

vscode/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@
388388
"none",
389389
"rbenv",
390390
"rvm",
391+
"rv",
391392
"shadowenv",
392393
"mise",
393394
"custom"
@@ -406,6 +407,10 @@
406407
"description": "The path to the rbenv executable, if not installed on one of the standard locations",
407408
"type": "string"
408409
},
410+
"rvExecutablePath": {
411+
"description": "The path to the rv executable, if not installed on one of the standard locations",
412+
"type": "string"
413+
},
409414
"chrubyRubies": {
410415
"description": "An array of extra directories to search for Ruby installations when using chruby. Equivalent to the RUBIES environment variable",
411416
"type": "array"

vscode/src/ruby.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Rvm } from "./ruby/rvm";
1515
import { None } from "./ruby/none";
1616
import { Custom } from "./ruby/custom";
1717
import { Asdf } from "./ruby/asdf";
18+
import { Rv } from "./ruby/rv";
1819

1920
async function detectMise() {
2021
const possiblePaths = [
@@ -44,6 +45,7 @@ export enum ManagerIdentifier {
4445
Shadowenv = "shadowenv",
4546
Mise = "mise",
4647
RubyInstaller = "rubyInstaller",
48+
Rv = "rv",
4749
None = "none",
4850
Custom = "custom",
4951
}
@@ -313,6 +315,11 @@ export class Ruby implements RubyInterface {
313315
new Mise(this.workspaceFolder, this.outputChannel, this.context, this.manuallySelectRuby.bind(this)),
314316
);
315317
break;
318+
case ManagerIdentifier.Rv:
319+
await this.runActivation(
320+
new Rv(this.workspaceFolder, this.outputChannel, this.context, this.manuallySelectRuby.bind(this)),
321+
);
322+
break;
316323
case ManagerIdentifier.RubyInstaller:
317324
await this.runActivation(
318325
new RubyInstaller(this.workspaceFolder, this.outputChannel, this.context, this.manuallySelectRuby.bind(this)),
@@ -362,7 +369,13 @@ export class Ruby implements RubyInterface {
362369
// If .shadowenv.d doesn't exist, then we check the other version managers
363370
}
364371

365-
const managers = [ManagerIdentifier.Chruby, ManagerIdentifier.Rbenv, ManagerIdentifier.Rvm, ManagerIdentifier.Asdf];
372+
const managers = [
373+
ManagerIdentifier.Chruby,
374+
ManagerIdentifier.Rbenv,
375+
ManagerIdentifier.Rvm,
376+
ManagerIdentifier.Asdf,
377+
ManagerIdentifier.Rv,
378+
];
366379

367380
for (const tool of managers) {
368381
const exists = await this.toolExists(tool);

vscode/src/ruby/rv.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import * as vscode from "vscode";
2+
3+
import { VersionManager, ActivationResult } from "./versionManager";
4+
5+
// Manage your Ruby environment with rv
6+
//
7+
// Learn more: https://github.com/spinel-coop/rv
8+
export class Rv extends VersionManager {
9+
async activate(): Promise<ActivationResult> {
10+
const rvExec = await this.findRv();
11+
const parsedResult = await this.runEnvActivationScript(`${rvExec} ruby run --`);
12+
13+
return {
14+
env: { ...process.env, ...parsedResult.env },
15+
yjit: parsedResult.yjit,
16+
version: parsedResult.version,
17+
gemPath: parsedResult.gemPath,
18+
};
19+
}
20+
21+
private async findRv(): Promise<string> {
22+
const config = vscode.workspace.getConfiguration("rubyLsp");
23+
const configuredRvPath = config.get<string | undefined>("rubyVersionManager.rvExecutablePath");
24+
25+
if (configuredRvPath) {
26+
return this.ensureRvExistsAt(configuredRvPath);
27+
} else {
28+
const possiblePaths = [
29+
vscode.Uri.joinPath(vscode.Uri.file("/"), "home", "linuxbrew", ".linuxbrew", "bin"),
30+
vscode.Uri.joinPath(vscode.Uri.file("/"), "usr", "local", "bin"),
31+
vscode.Uri.joinPath(vscode.Uri.file("/"), "opt", "homebrew", "bin"),
32+
vscode.Uri.joinPath(vscode.Uri.file("/"), "usr", "bin"),
33+
];
34+
return this.findExec(possiblePaths, "rv");
35+
}
36+
}
37+
38+
private async ensureRvExistsAt(path: string): Promise<string> {
39+
try {
40+
await vscode.workspace.fs.stat(vscode.Uri.file(path));
41+
return path;
42+
} catch (_error: any) {
43+
throw new Error(`The Ruby LSP version manager is configured to be rv, but ${path} does not exist`);
44+
}
45+
}
46+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import assert from "assert";
2+
import path from "path";
3+
import os from "os";
4+
import fs from "fs";
5+
6+
import * as vscode from "vscode";
7+
import sinon from "sinon";
8+
import { afterEach, beforeEach } from "mocha";
9+
10+
import { Rv } from "../../../ruby/rv";
11+
import { WorkspaceChannel } from "../../../workspaceChannel";
12+
import * as common from "../../../common";
13+
import { ACTIVATION_SEPARATOR, FIELD_SEPARATOR, VALUE_SEPARATOR } from "../../../ruby/versionManager";
14+
import { createContext, FakeContext } from "../helpers";
15+
16+
suite("Rv", () => {
17+
if (os.platform() === "win32") {
18+
// eslint-disable-next-line no-console
19+
console.log("Skipping Rv tests on Windows");
20+
return;
21+
}
22+
23+
let activationPath: vscode.Uri;
24+
let sandbox: sinon.SinonSandbox;
25+
let context: FakeContext;
26+
27+
beforeEach(() => {
28+
sandbox = sinon.createSandbox();
29+
context = createContext();
30+
activationPath = vscode.Uri.joinPath(context.extensionUri, "activation.rb");
31+
});
32+
33+
afterEach(() => {
34+
sandbox.restore();
35+
context.dispose();
36+
});
37+
38+
test("Activates with auto-detected version", async () => {
39+
const workspacePath = process.env.PWD!;
40+
const workspaceFolder = {
41+
uri: vscode.Uri.from({ scheme: "file", path: workspacePath }),
42+
name: path.basename(workspacePath),
43+
index: 0,
44+
};
45+
const outputChannel = new WorkspaceChannel("fake", common.LOG_CHANNEL);
46+
47+
const rv = new Rv(workspaceFolder, outputChannel, context, async () => {});
48+
49+
const envStub = ["3.4.8", "/path/to/gems", "true", `ANY${VALUE_SEPARATOR}true`].join(FIELD_SEPARATOR);
50+
51+
const execStub = sandbox.stub(common, "asyncExec").resolves({
52+
stdout: "",
53+
stderr: `${ACTIVATION_SEPARATOR}${envStub}${ACTIVATION_SEPARATOR}`,
54+
});
55+
56+
// Stub findRv to return the executable path
57+
sandbox.stub(rv, "findRv" as any).resolves("rv");
58+
59+
const { env, version, yjit } = await rv.activate();
60+
61+
assert.ok(
62+
execStub.calledOnceWithExactly(`rv ruby run -- -EUTF-8:UTF-8 '${activationPath.fsPath}'`, {
63+
cwd: workspacePath,
64+
shell: vscode.env.shell,
65+
66+
env: process.env,
67+
encoding: "utf-8",
68+
}),
69+
);
70+
71+
assert.strictEqual(version, "3.4.8");
72+
assert.strictEqual(yjit, true);
73+
assert.strictEqual(env.ANY, "true");
74+
});
75+
76+
test("Allows configuring where rv is installed", async () => {
77+
const workspacePath = fs.mkdtempSync(path.join(os.tmpdir(), "ruby-lsp-test-"));
78+
const workspaceFolder = {
79+
uri: vscode.Uri.from({ scheme: "file", path: workspacePath }),
80+
name: path.basename(workspacePath),
81+
index: 0,
82+
};
83+
const outputChannel = new WorkspaceChannel("fake", common.LOG_CHANNEL);
84+
85+
const rv = new Rv(workspaceFolder, outputChannel, context, async () => {});
86+
87+
const envStub = ["3.4.8", "/path/to/gems", "true", `ANY${VALUE_SEPARATOR}true`].join(FIELD_SEPARATOR);
88+
89+
const execStub = sandbox.stub(common, "asyncExec").resolves({
90+
stdout: "",
91+
stderr: `${ACTIVATION_SEPARATOR}${envStub}${ACTIVATION_SEPARATOR}`,
92+
});
93+
94+
const rvPath = path.join(workspacePath, "rv");
95+
fs.writeFileSync(rvPath, "fakeRvBinary");
96+
97+
// Stub findRv to return the configured executable path
98+
sandbox.stub(rv, "findRv" as any).resolves(rvPath);
99+
100+
const configStub = sinon.stub(vscode.workspace, "getConfiguration").returns({
101+
get: (name: string) => {
102+
if (name === "rubyVersionManager.rvExecutablePath") {
103+
return rvPath;
104+
}
105+
return "";
106+
},
107+
} as any);
108+
109+
const { env, version, yjit } = await rv.activate();
110+
111+
assert.ok(
112+
execStub.calledOnceWithExactly(`${rvPath} ruby run -- -EUTF-8:UTF-8 '${activationPath.fsPath}'`, {
113+
cwd: workspacePath,
114+
shell: vscode.env.shell,
115+
116+
env: process.env,
117+
encoding: "utf-8",
118+
}),
119+
);
120+
121+
assert.strictEqual(version, "3.4.8");
122+
assert.strictEqual(yjit, true);
123+
assert.deepStrictEqual(env.ANY, "true");
124+
125+
execStub.restore();
126+
configStub.restore();
127+
fs.rmSync(workspacePath, { recursive: true, force: true });
128+
});
129+
});

0 commit comments

Comments
 (0)