From c5e248dcf19bf2237aeba841a4718cafea03bdbf Mon Sep 17 00:00:00 2001 From: Vinicius Stock Date: Thu, 17 Oct 2024 10:06:46 -0400 Subject: [PATCH] Use activated gem paths to register document selectors (#2718) * Return gem path from activation scripts * Use activated gem paths to register document selectors --- vscode/src/client.ts | 36 ++++++++-------- vscode/src/ruby.ts | 4 +- vscode/src/ruby/asdf.ts | 1 + vscode/src/ruby/chruby.ts | 1 + vscode/src/ruby/custom.ts | 1 + vscode/src/ruby/mise.ts | 1 + vscode/src/ruby/none.ts | 1 + vscode/src/ruby/rbenv.ts | 1 + vscode/src/ruby/rvm.ts | 1 + vscode/src/ruby/shadowenv.ts | 1 + vscode/src/ruby/versionManager.ts | 3 +- vscode/src/test/suite/ruby.test.ts | 66 ++++++++++++++++++++++-------- 12 files changed, 77 insertions(+), 40 deletions(-) diff --git a/vscode/src/client.ts b/vscode/src/client.ts index c833dc0bf..b687be4ed 100644 --- a/vscode/src/client.ts +++ b/vscode/src/client.ts @@ -126,6 +126,11 @@ function collectClientOptions( const enabledFeatures = Object.keys(features).filter((key) => features[key]); const fsPath = workspaceFolder.uri.fsPath.replace(/\/$/, ""); + + // For each workspace, the language client is responsible for handling requests for: + // 1. Files inside of the workspace itself + // 2. Bundled gems + // 3. Default gems let documentSelector: DocumentSelector = SUPPORTED_LANGUAGE_IDS.map( (language) => { return { language, pattern: `${fsPath}/**/*` }; @@ -143,27 +148,18 @@ function collectClientOptions( }); } - // For each workspace, the language client is responsible for handling requests for: - // 1. Files inside of the workspace itself - // 2. Bundled gems - // 3. Default gems - - if (ruby.env.GEM_PATH) { - const parts = ruby.env.GEM_PATH.split(path.delimiter); - - // Because of how default gems are installed, the entry in the `GEM_PATH` is actually not exactly where the files - // are located. With the regex, we are correcting the default gem path from this (where the files are not located) - // /opt/rubies/3.3.1/lib/ruby/gems/3.3.0 - // - // to this (where the files are actually stored) - // /opt/rubies/3.3.1/lib/ruby/3.3.0 - parts.forEach((gemPath) => { - documentSelector.push({ - language: "ruby", - pattern: `${gemPath.replace(/lib\/ruby\/gems\/(?=\d)/, "lib/ruby/")}/**/*`, - }); + // Because of how default gems are installed, the entry in the `GEM_PATH` is actually not exactly where the files + // are located. With the regex, we are correcting the default gem path from this (where the files are not located) + // /opt/rubies/3.3.1/lib/ruby/gems/3.3.0 + // + // to this (where the files are actually stored) + // /opt/rubies/3.3.1/lib/ruby/3.3.0 + ruby.gemPath.forEach((gemPath) => { + documentSelector.push({ + language: "ruby", + pattern: `${gemPath.replace(/lib\/ruby\/gems\/(?=\d)/, "lib/ruby/")}/**/*`, }); - } + }); // This is a temporary solution as an escape hatch for users who cannot upgrade the `ruby-lsp` gem to a version that // supports ERB diff --git a/vscode/src/ruby.ts b/vscode/src/ruby.ts index 8ba6d502f..42e190bc4 100644 --- a/vscode/src/ruby.ts +++ b/vscode/src/ruby.ts @@ -39,6 +39,7 @@ export class Ruby implements RubyInterface { // This property indicates that Ruby has been compiled with YJIT support and that we're running on a Ruby version // where it will be activated, either by the extension or by the server public yjitEnabled?: boolean; + readonly gemPath: string[] = []; private readonly workspaceFolder: vscode.WorkspaceFolder; #versionManager: ManagerConfiguration = vscode.workspace .getConfiguration("rubyLsp") @@ -201,7 +202,7 @@ export class Ruby implements RubyInterface { } private async runActivation(manager: VersionManager) { - const { env, version, yjit } = await manager.activate(); + const { env, version, yjit, gemPath } = await manager.activate(); const [major, minor, _patch] = version.split(".").map(Number); this.sanitizeEnvironment(env); @@ -211,6 +212,7 @@ export class Ruby implements RubyInterface { this._env = env; this.rubyVersion = version; this.yjitEnabled = (yjit && major > 3) || (major === 3 && minor >= 2); + this.gemPath.push(...gemPath); } // Fetch information related to the Ruby version. This can only be invoked after activation, so that `rubyVersion` is diff --git a/vscode/src/ruby/asdf.ts b/vscode/src/ruby/asdf.ts index 6ed9bbf89..32c7e5f0e 100644 --- a/vscode/src/ruby/asdf.ts +++ b/vscode/src/ruby/asdf.ts @@ -21,6 +21,7 @@ export class Asdf extends VersionManager { env: { ...process.env, ...parsedResult.env }, yjit: parsedResult.yjit, version: parsedResult.version, + gemPath: parsedResult.gemPath, }; } diff --git a/vscode/src/ruby/chruby.ts b/vscode/src/ruby/chruby.ts index 6048ca30a..cabb26ee0 100644 --- a/vscode/src/ruby/chruby.ts +++ b/vscode/src/ruby/chruby.ts @@ -70,6 +70,7 @@ export class Chruby extends VersionManager { env: { ...process.env, ...rubyEnv }, yjit, version, + gemPath: [gemHome, defaultGems], }; } diff --git a/vscode/src/ruby/custom.ts b/vscode/src/ruby/custom.ts index 85e5e7674..d7126385c 100644 --- a/vscode/src/ruby/custom.ts +++ b/vscode/src/ruby/custom.ts @@ -18,6 +18,7 @@ export class Custom extends VersionManager { env: { ...process.env, ...parsedResult.env }, yjit: parsedResult.yjit, version: parsedResult.version, + gemPath: parsedResult.gemPath, }; } diff --git a/vscode/src/ruby/mise.ts b/vscode/src/ruby/mise.ts index 6b01a4bbd..3695b2677 100644 --- a/vscode/src/ruby/mise.ts +++ b/vscode/src/ruby/mise.ts @@ -21,6 +21,7 @@ export class Mise extends VersionManager { env: { ...process.env, ...parsedResult.env }, yjit: parsedResult.yjit, version: parsedResult.version, + gemPath: parsedResult.gemPath, }; } diff --git a/vscode/src/ruby/none.ts b/vscode/src/ruby/none.ts index 0bc8607bd..faea1690f 100644 --- a/vscode/src/ruby/none.ts +++ b/vscode/src/ruby/none.ts @@ -32,6 +32,7 @@ export class None extends VersionManager { env: { ...process.env, ...parsedResult.env }, yjit: parsedResult.yjit, version: parsedResult.version, + gemPath: parsedResult.gemPath, }; } } diff --git a/vscode/src/ruby/rbenv.ts b/vscode/src/ruby/rbenv.ts index 6c0955446..de4c541ac 100644 --- a/vscode/src/ruby/rbenv.ts +++ b/vscode/src/ruby/rbenv.ts @@ -13,6 +13,7 @@ export class Rbenv extends VersionManager { env: { ...process.env, ...parsedResult.env }, yjit: parsedResult.yjit, version: parsedResult.version, + gemPath: parsedResult.gemPath, }; } } diff --git a/vscode/src/ruby/rvm.ts b/vscode/src/ruby/rvm.ts index 7a18b2d1f..94c28c89c 100644 --- a/vscode/src/ruby/rvm.ts +++ b/vscode/src/ruby/rvm.ts @@ -26,6 +26,7 @@ export class Rvm extends VersionManager { env: { ...process.env, ...parsedResult.env }, yjit: parsedResult.yjit, version: parsedResult.version, + gemPath: parsedResult.gemPath, }; } diff --git a/vscode/src/ruby/shadowenv.ts b/vscode/src/ruby/shadowenv.ts index e1dd5f432..3e716b026 100644 --- a/vscode/src/ruby/shadowenv.ts +++ b/vscode/src/ruby/shadowenv.ts @@ -31,6 +31,7 @@ export class Shadowenv extends VersionManager { env: { ...process.env, ...parsedResult.env }, yjit: parsedResult.yjit, version: parsedResult.version, + gemPath: parsedResult.gemPath, }; } catch (error: any) { const { stdout } = await this.runScript("command -v shadowenv"); diff --git a/vscode/src/ruby/versionManager.ts b/vscode/src/ruby/versionManager.ts index a0bbe69e7..428fb5c82 100644 --- a/vscode/src/ruby/versionManager.ts +++ b/vscode/src/ruby/versionManager.ts @@ -11,6 +11,7 @@ export interface ActivationResult { env: NodeJS.ProcessEnv; yjit: boolean; version: string; + gemPath: string[]; } export const ACTIVATION_SEPARATOR = "RUBY_LSP_ACTIVATION_SEPARATOR"; @@ -18,7 +19,7 @@ export const ACTIVATION_SEPARATOR = "RUBY_LSP_ACTIVATION_SEPARATOR"; export abstract class VersionManager { public activationScript = [ `STDERR.print("${ACTIVATION_SEPARATOR}" + `, - "{ env: ENV.to_h, yjit: !!defined?(RubyVM:: YJIT), version: RUBY_VERSION }.to_json + ", + "{ env: ENV.to_h, yjit: !!defined?(RubyVM:: YJIT), version: RUBY_VERSION, gemPath: Gem.path }.to_json + ", `"${ACTIVATION_SEPARATOR}")`, ].join(""); diff --git a/vscode/src/test/suite/ruby.test.ts b/vscode/src/test/suite/ruby.test.ts index 7da73e64c..5180c8049 100644 --- a/vscode/src/test/suite/ruby.test.ts +++ b/vscode/src/test/suite/ruby.test.ts @@ -8,6 +8,8 @@ import sinon from "sinon"; import { Ruby, ManagerIdentifier } from "../../ruby"; import { WorkspaceChannel } from "../../workspaceChannel"; import { LOG_CHANNEL } from "../../common"; +import * as common from "../../common"; +import { ACTIVATION_SEPARATOR } from "../../ruby/versionManager"; suite("Ruby environment activation", () => { const workspacePath = path.dirname( @@ -18,6 +20,14 @@ suite("Ruby environment activation", () => { name: path.basename(workspacePath), index: 0, }; + const context = { + extensionMode: vscode.ExtensionMode.Test, + workspaceState: { + get: () => undefined, + update: () => undefined, + }, + } as unknown as vscode.ExtensionContext; + const outputChannel = new WorkspaceChannel("fake", LOG_CHANNEL); test("Activate fetches Ruby information when outside of Ruby LSP", async () => { const manager = process.env.CI @@ -38,15 +48,6 @@ suite("Ruby environment activation", () => { }, } as unknown as vscode.WorkspaceConfiguration); - const context = { - extensionMode: vscode.ExtensionMode.Test, - workspaceState: { - get: () => undefined, - update: () => undefined, - }, - } as unknown as vscode.ExtensionContext; - const outputChannel = new WorkspaceChannel("fake", LOG_CHANNEL); - const ruby = new Ruby(context, workspaceFolder, outputChannel); await ruby.activateRuby(); @@ -79,15 +80,6 @@ suite("Ruby environment activation", () => { }, } as unknown as vscode.WorkspaceConfiguration); - const context = { - extensionMode: vscode.ExtensionMode.Test, - workspaceState: { - get: () => undefined, - update: () => undefined, - }, - } as unknown as vscode.ExtensionContext; - const outputChannel = new WorkspaceChannel("fake", LOG_CHANNEL); - const ruby = new Ruby(context, workspaceFolder, outputChannel); process.env.VERBOSE = "1"; @@ -103,4 +95,42 @@ suite("Ruby environment activation", () => { delete process.env.RUBY_GC_HEAP_GROWTH_FACTOR; configStub.restore(); }); + + test("Sets gem path for version managers based on shims", async () => { + const configStub = sinon + .stub(vscode.workspace, "getConfiguration") + .returns({ + get: (name: string) => { + if (name === "rubyVersionManager") { + return { identifier: ManagerIdentifier.Rbenv }; + } else if (name === "bundleGemfile") { + return ""; + } + + return undefined; + }, + } as unknown as vscode.WorkspaceConfiguration); + + const envStub = { + env: { ANY: "true" }, + yjit: true, + version: "3.3.5", + gemPath: ["~/.gem/ruby/3.3.5", "/opt/rubies/3.3.5/lib/ruby/gems/3.3.0"], + }; + + const execStub = sinon.stub(common, "asyncExec").resolves({ + stdout: "", + stderr: `${ACTIVATION_SEPARATOR}${JSON.stringify(envStub)}${ACTIVATION_SEPARATOR}`, + }); + + const ruby = new Ruby(context, workspaceFolder, outputChannel); + await ruby.activateRuby(); + execStub.restore(); + configStub.restore(); + + assert.deepStrictEqual(ruby.gemPath, [ + "~/.gem/ruby/3.3.5", + "/opt/rubies/3.3.5/lib/ruby/gems/3.3.0", + ]); + }); });