diff --git a/vscode/src/ruby.ts b/vscode/src/ruby.ts index d0a6d8371..c1230cfd7 100644 --- a/vscode/src/ruby.ts +++ b/vscode/src/ruby.ts @@ -83,12 +83,11 @@ export class Ruby implements RubyInterface { this.outputChannel = outputChannel; this.telemetry = telemetry; - const customBundleGemfile: string = vscode.workspace.getConfiguration("rubyLsp").get("bundleGemfile")!; + const rawBundleGemfile: string = vscode.workspace.getConfiguration("rubyLsp").get("bundleGemfile")!; + const customBundleGemfile = rawBundleGemfile.replace(/\$\{workspaceFolder\}/g, this.workspaceFolder.uri.fsPath); if (customBundleGemfile.length > 0) { - this.customBundleGemfile = path.isAbsolute(customBundleGemfile) - ? customBundleGemfile - : path.resolve(path.join(this.workspaceFolder.uri.fsPath, customBundleGemfile)); + this.customBundleGemfile = path.resolve(this.workspaceFolder.uri.fsPath, customBundleGemfile); } } @@ -120,6 +119,14 @@ export class Ruby implements RubyInterface { this.versionManager = versionManager; this._error = false; + if (this.customBundleGemfile) { + try { + await vscode.workspace.fs.stat(vscode.Uri.file(this.customBundleGemfile)); + } catch (_error: any) { + throw new Error(`The configured bundle gemfile ${this.customBundleGemfile} does not exist`); + } + } + const workspaceRubyPath = await this.cachedWorkspaceRubyPath(); if (workspaceRubyPath) { @@ -131,6 +138,7 @@ export class Ruby implements RubyInterface { this.context, this.manuallySelectRuby.bind(this), workspaceRubyPath, + this.customBundleGemfile, ), ); } else { @@ -165,6 +173,7 @@ export class Ruby implements RubyInterface { this.context, this.manuallySelectRuby.bind(this), globalRubyPath, + this.customBundleGemfile, ), ); } else { @@ -183,7 +192,7 @@ export class Ruby implements RubyInterface { if (!this.error) { this.fetchRubyVersionInfo(); - await this.setupBundlePath(); + this.setupBundlePath(); } } @@ -288,72 +297,63 @@ export class Ruby implements RubyInterface { } private async runManagerActivation() { + const manuallySelectRuby = this.manuallySelectRuby.bind(this); + const args = [ + this.workspaceFolder, + this.outputChannel, + this.context, + manuallySelectRuby, + this.customBundleGemfile, + ] as const; + switch (this.versionManager.identifier) { case ManagerIdentifier.Asdf: - await this.runActivation( - new Asdf(this.workspaceFolder, this.outputChannel, this.context, this.manuallySelectRuby.bind(this)), - ); + await this.runActivation(new Asdf(...args)); break; case ManagerIdentifier.Chruby: - await this.runActivation( - new Chruby(this.workspaceFolder, this.outputChannel, this.context, this.manuallySelectRuby.bind(this)), - ); + await this.runActivation(new Chruby(...args)); break; case ManagerIdentifier.Rbenv: - await this.runActivation( - new Rbenv(this.workspaceFolder, this.outputChannel, this.context, this.manuallySelectRuby.bind(this)), - ); + await this.runActivation(new Rbenv(...args)); break; case ManagerIdentifier.Rvm: - await this.runActivation( - new Rvm(this.workspaceFolder, this.outputChannel, this.context, this.manuallySelectRuby.bind(this)), - ); + await this.runActivation(new Rvm(...args)); break; case ManagerIdentifier.Mise: - await this.runActivation( - new Mise(this.workspaceFolder, this.outputChannel, this.context, this.manuallySelectRuby.bind(this)), - ); + await this.runActivation(new Mise(...args)); break; case ManagerIdentifier.Rv: - await this.runActivation( - new Rv(this.workspaceFolder, this.outputChannel, this.context, this.manuallySelectRuby.bind(this)), - ); + await this.runActivation(new Rv(...args)); break; case ManagerIdentifier.RubyInstaller: - await this.runActivation( - new RubyInstaller(this.workspaceFolder, this.outputChannel, this.context, this.manuallySelectRuby.bind(this)), - ); + await this.runActivation(new RubyInstaller(...args)); break; case ManagerIdentifier.Custom: - await this.runActivation( - new Custom(this.workspaceFolder, this.outputChannel, this.context, this.manuallySelectRuby.bind(this)), - ); + await this.runActivation(new Custom(...args)); break; case ManagerIdentifier.None: await this.runActivation( - new None(this.workspaceFolder, this.outputChannel, this.context, this.manuallySelectRuby.bind(this)), + new None( + this.workspaceFolder, + this.outputChannel, + this.context, + manuallySelectRuby, + undefined, + this.customBundleGemfile, + ), ); break; default: - await this.runActivation( - new Shadowenv(this.workspaceFolder, this.outputChannel, this.context, this.manuallySelectRuby.bind(this)), - ); + await this.runActivation(new Shadowenv(...args)); break; } } - private async setupBundlePath() { + private setupBundlePath() { // Some users like to define a completely separate Gemfile for development tools. We allow them to use // `rubyLsp.bundleGemfile` to configure that and need to inject it into the environment - if (!this.customBundleGemfile) { - return; - } - - try { - await vscode.workspace.fs.stat(vscode.Uri.file(this.customBundleGemfile)); + if (this.customBundleGemfile) { this._env.BUNDLE_GEMFILE = this.customBundleGemfile; - } catch (_error: any) { - throw new Error(`The configured bundle gemfile ${this.customBundleGemfile} does not exist`); } } diff --git a/vscode/src/ruby/chruby.ts b/vscode/src/ruby/chruby.ts index 0036efb67..dc1f73a58 100644 --- a/vscode/src/ruby/chruby.ts +++ b/vscode/src/ruby/chruby.ts @@ -28,8 +28,9 @@ export class Chruby extends VersionManager { outputChannel: WorkspaceChannel, context: vscode.ExtensionContext, manuallySelectRuby: () => Promise, + customBundleGemfile?: string, ) { - super(workspaceFolder, outputChannel, context, manuallySelectRuby); + super(workspaceFolder, outputChannel, context, manuallySelectRuby, customBundleGemfile); const configuredRubies = vscode.workspace .getConfiguration("rubyLsp") diff --git a/vscode/src/ruby/none.ts b/vscode/src/ruby/none.ts index 0c6a8c7e5..4db0e8085 100644 --- a/vscode/src/ruby/none.ts +++ b/vscode/src/ruby/none.ts @@ -21,8 +21,9 @@ export class None extends VersionManager { context: vscode.ExtensionContext, manuallySelectRuby: () => Promise, rubyPath?: string, + customBundleGemfile?: string, ) { - super(workspaceFolder, outputChannel, context, manuallySelectRuby); + super(workspaceFolder, outputChannel, context, manuallySelectRuby, customBundleGemfile); this.rubyPath = rubyPath ?? "ruby"; } diff --git a/vscode/src/ruby/versionManager.ts b/vscode/src/ruby/versionManager.ts index 66c591ce7..52fa4b94f 100644 --- a/vscode/src/ruby/versionManager.ts +++ b/vscode/src/ruby/versionManager.ts @@ -31,18 +31,13 @@ export abstract class VersionManager { outputChannel: WorkspaceChannel, context: vscode.ExtensionContext, manuallySelectRuby: () => Promise, + customBundleGemfile?: string, ) { this.workspaceFolder = workspaceFolder; this.outputChannel = outputChannel; this.context = context; this.manuallySelectRuby = manuallySelectRuby; - const customBundleGemfile: string = vscode.workspace.getConfiguration("rubyLsp").get("bundleGemfile")!; - - if (customBundleGemfile.length > 0) { - this.customBundleGemfile = path.isAbsolute(customBundleGemfile) - ? customBundleGemfile - : path.resolve(path.join(this.workspaceFolder.uri.fsPath, customBundleGemfile)); - } + this.customBundleGemfile = customBundleGemfile; this.bundleUri = this.customBundleGemfile ? vscode.Uri.file(path.dirname(this.customBundleGemfile)) diff --git a/vscode/src/test/suite/common.test.ts b/vscode/src/test/suite/common.test.ts index b4588bc7c..954797fe4 100644 --- a/vscode/src/test/suite/common.test.ts +++ b/vscode/src/test/suite/common.test.ts @@ -5,7 +5,7 @@ import sinon from "sinon"; import { featureEnabled, FEATURE_FLAGS } from "../../common"; -suite("Common", () => { +suite("featureEnabled", () => { let sandbox: sinon.SinonSandbox; setup(() => { diff --git a/vscode/src/test/suite/ruby.test.ts b/vscode/src/test/suite/ruby.test.ts index b53f4b139..09bfda8b5 100644 --- a/vscode/src/test/suite/ruby.test.ts +++ b/vscode/src/test/suite/ruby.test.ts @@ -1,5 +1,7 @@ import * as assert from "assert"; import * as path from "path"; +import * as os from "os"; +import * as fs from "fs"; import * as vscode from "vscode"; import sinon from "sinon"; @@ -169,4 +171,81 @@ suite("Ruby environment activation", () => { assert.strictEqual(context.workspaceState.get(`rubyLsp.workspaceRubyPath.${workspaceFolder.name}`), undefined); }); + + // eslint-disable-next-line no-template-curly-in-string + test("Expands ${workspaceFolder} in bundleGemfile setting", async () => { + const tmpPath = fs.mkdtempSync(path.join(os.tmpdir(), "ruby-lsp-test-")); + // Use the URI's fsPath to normalize the drive letter casing on Windows (e.g. c: -> C:) + const normalizedTmpPath = vscode.Uri.file(tmpPath).fsPath; + const gemfilePath = path.resolve(normalizedTmpPath, "Gemfile"); + fs.writeFileSync(gemfilePath, ""); + + const tmpWorkspaceFolder: vscode.WorkspaceFolder = { + uri: vscode.Uri.file(tmpPath), + name: path.basename(tmpPath), + index: 0, + }; + + sandbox.stub(vscode.workspace, "getConfiguration").returns({ + get: (name: string) => { + if (name === "rubyVersionManager") { + return { identifier: ManagerIdentifier.None }; + } else if (name === "bundleGemfile") { + // eslint-disable-next-line no-template-curly-in-string + return "${workspaceFolder}/Gemfile"; + } + + return undefined; + }, + } as unknown as vscode.WorkspaceConfiguration); + + const envStub = [ + "3.3.5", + "~/.gem/ruby/3.3.5,/opt/rubies/3.3.5/lib/ruby/gems/3.3.0", + "true", + `ANY${VALUE_SEPARATOR}true`, + ].join(FIELD_SEPARATOR); + + sandbox.stub(common, "asyncExec").resolves({ + stdout: "", + stderr: `${ACTIVATION_SEPARATOR}${envStub}${ACTIVATION_SEPARATOR}`, + }); + + const ruby = new Ruby(context, tmpWorkspaceFolder, outputChannel, FAKE_TELEMETRY); + await ruby.activateRuby(); + + assert.strictEqual(ruby.env.BUNDLE_GEMFILE, gemfilePath); + fs.rmSync(tmpPath, { recursive: true, force: true }); + }); + + test("Raises an error if the configured bundleGemfile does not exist", async () => { + const tmpPath = fs.mkdtempSync(path.join(os.tmpdir(), "ruby-lsp-test-")); + const tmpWorkspaceFolder: vscode.WorkspaceFolder = { + uri: vscode.Uri.file(tmpPath), + name: path.basename(tmpPath), + index: 0, + }; + + const nonExistentGemfile = path.join(tmpPath, "nonexistent", "Gemfile"); + + sandbox.stub(vscode.workspace, "getConfiguration").returns({ + get: (name: string) => { + if (name === "bundleGemfile") { + return nonExistentGemfile; + } else if (name === "rubyVersionManager") { + return { identifier: ManagerIdentifier.None }; + } + + return undefined; + }, + } as unknown as vscode.WorkspaceConfiguration); + + const ruby = new Ruby(context, tmpWorkspaceFolder, outputChannel, FAKE_TELEMETRY); + + await assert.rejects(() => ruby.activateRuby(), { + message: `The configured bundle gemfile ${nonExistentGemfile} does not exist`, + }); + + fs.rmSync(tmpPath, { recursive: true, force: true }); + }); });