diff --git a/vscode/package.json b/vscode/package.json index f7641c617..844d62cba 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -345,6 +345,10 @@ "description": "The path to the Mise executable, if not installed in ~/.local/bin/mise", "type": "string" }, + "rbenvExecutablePath": { + "description": "The path to the rbenv executable, if not installed on one of the standard locations", + "type": "string" + }, "chrubyRubies": { "description": "An array of extra directories to search for Ruby installations when using chruby. Equivalent to the RUBIES environment variable", "type": "array" diff --git a/vscode/src/ruby/rbenv.ts b/vscode/src/ruby/rbenv.ts index b58d9cfee..2666db0d2 100644 --- a/vscode/src/ruby/rbenv.ts +++ b/vscode/src/ruby/rbenv.ts @@ -8,10 +8,7 @@ import { VersionManager, ActivationResult } from "./versionManager"; // Learn more: https://github.com/rbenv/rbenv export class Rbenv extends VersionManager { async activate(): Promise { - const rbenvExec = await this.findExec( - [vscode.Uri.file("/opt/homebrew/bin"), vscode.Uri.file("/usr/local/bin")], - "rbenv", - ); + const rbenvExec = await this.findRbenv(); const parsedResult = await this.runEnvActivationScript( `${rbenvExec} exec ruby`, @@ -24,4 +21,35 @@ export class Rbenv extends VersionManager { gemPath: parsedResult.gemPath, }; } + + private async findRbenv(): Promise { + const config = vscode.workspace.getConfiguration("rubyLsp"); + const configuredRbenvPath = config.get( + "rubyVersionManager.rbenvExecutablePath", + ); + + if (configuredRbenvPath) { + return this.ensureRbenvExistsAt(configuredRbenvPath); + } else { + return this.findExec( + [ + vscode.Uri.file("/opt/homebrew/bin"), + vscode.Uri.file("/usr/local/bin"), + ], + "rbenv", + ); + } + } + + private async ensureRbenvExistsAt(path: string): Promise { + try { + await vscode.workspace.fs.stat(vscode.Uri.file(path)); + + return path; + } catch (error: any) { + throw new Error( + `The Ruby LSP version manager is configured to be rbenv, but ${path} does not exist`, + ); + } + } } diff --git a/vscode/src/test/suite/ruby/rbenv.test.ts b/vscode/src/test/suite/ruby/rbenv.test.ts index db33705f8..a39a12cea 100644 --- a/vscode/src/test/suite/ruby/rbenv.test.ts +++ b/vscode/src/test/suite/ruby/rbenv.test.ts @@ -1,6 +1,7 @@ import assert from "assert"; import path from "path"; import os from "os"; +import fs from "fs"; import * as vscode from "vscode"; import sinon from "sinon"; @@ -59,6 +60,66 @@ suite("Rbenv", () => { execStub.restore(); }); + test("Allows configuring where rbenv is installed", async () => { + const workspacePath = fs.mkdtempSync( + path.join(os.tmpdir(), "ruby-lsp-test-"), + ); + const workspaceFolder = { + uri: vscode.Uri.from({ scheme: "file", path: workspacePath }), + name: path.basename(workspacePath), + index: 0, + }; + const outputChannel = new WorkspaceChannel("fake", common.LOG_CHANNEL); + const rbenv = new Rbenv(workspaceFolder, outputChannel); + + const envStub = { + env: { ANY: "true" }, + yjit: true, + version: "3.0.0", + }; + + const execStub = sinon.stub(common, "asyncExec").resolves({ + stdout: "", + stderr: `${ACTIVATION_SEPARATOR}${JSON.stringify(envStub)}${ACTIVATION_SEPARATOR}`, + }); + + const rbenvPath = path.join(workspacePath, "rbenv"); + fs.writeFileSync(rbenvPath, "fakeRbenvBinary"); + + const configStub = sinon + .stub(vscode.workspace, "getConfiguration") + .returns({ + get: (name: string) => { + if (name === "rubyVersionManager.rbenvExecutablePath") { + return rbenvPath; + } + return ""; + }, + } as any); + + const { env, version, yjit } = await rbenv.activate(); + + assert.ok( + execStub.calledOnceWithExactly( + `${rbenvPath} exec ruby -W0 -rjson -e '${rbenv.activationScript}'`, + { + cwd: workspacePath, + shell: vscode.env.shell, + // eslint-disable-next-line no-process-env + env: process.env, + }, + ), + ); + + assert.strictEqual(version, "3.0.0"); + assert.strictEqual(yjit, true); + assert.deepStrictEqual(env.ANY, "true"); + + execStub.restore(); + configStub.restore(); + fs.rmSync(workspacePath, { recursive: true, force: true }); + }); + test("Reports invalid JSON environments", async () => { // eslint-disable-next-line no-process-env const workspacePath = process.env.PWD!;