Skip to content

Commit

Permalink
Add manual Ruby configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed May 1, 2024
1 parent b97e2a0 commit ce9cdf6
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 84 deletions.
4 changes: 4 additions & 0 deletions vscode/package.json
Expand Up @@ -318,6 +318,10 @@
"description": "Ignores if the project uses a typechecker. Only intended to be used while working on the Ruby LSP itself",
"type": "boolean",
"default": false
},
"rubyLsp.rubyExecutablePath": {
"description": "Path to the Ruby installation. This is used as a fallback if version manager activation fails",
"type": "string"
}
}
},
Expand Down
216 changes: 155 additions & 61 deletions vscode/src/ruby.ts
Expand Up @@ -101,80 +101,103 @@ export class Ruby implements RubyInterface {
.get<ManagerConfiguration>("rubyVersionManager")!,
) {
this.versionManager = versionManager;
this._error = false;

// If the version manager is auto, discover the actual manager before trying to activate anything
if (this.versionManager.identifier === ManagerIdentifier.Auto) {
await this.discoverVersionManager();
this.outputChannel.info(
`Discovered version manager ${this.versionManager.identifier}`,
const workspaceRubyPath = this.context.workspaceState.get<
string | undefined
>(`rubyLsp.workspaceRubyPath.${this.workspaceFolder.name}`);

if (workspaceRubyPath) {
// If a workspace specific Ruby path is configured, then we use that to activate the environment
await this.runActivation(
new None(this.workspaceFolder, this.outputChannel, workspaceRubyPath),
);
}
} else {
// If the version manager is auto, discover the actual manager before trying to activate anything
if (this.versionManager.identifier === ManagerIdentifier.Auto) {
await this.discoverVersionManager();
this.outputChannel.info(
`Discovered version manager ${this.versionManager.identifier}`,
);
}

try {
switch (this.versionManager.identifier) {
case ManagerIdentifier.Asdf:
await this.runActivation(
new Asdf(this.workspaceFolder, this.outputChannel),
);
break;
case ManagerIdentifier.Chruby:
await this.runActivation(
new Chruby(this.workspaceFolder, this.outputChannel),
);
break;
case ManagerIdentifier.Rbenv:
await this.runActivation(
new Rbenv(this.workspaceFolder, this.outputChannel),
);
break;
case ManagerIdentifier.Rvm:
await this.runActivation(
new Rvm(this.workspaceFolder, this.outputChannel),
);
break;
case ManagerIdentifier.Mise:
await this.runActivation(
new Mise(this.workspaceFolder, this.outputChannel),
);
break;
case ManagerIdentifier.RubyInstaller:
await this.runActivation(
new RubyInstaller(this.workspaceFolder, this.outputChannel),
);
break;
case ManagerIdentifier.Custom:
await this.runActivation(
new Custom(this.workspaceFolder, this.outputChannel),
);
break;
case ManagerIdentifier.None:
await this.runActivation(
new None(this.workspaceFolder, this.outputChannel),
);
break;
default:
try {
await this.runManagerActivation();
} catch (error: any) {
// If an error occurred and a global Ruby path is configured, then we can try to fallback to that
const globalRubyPath = vscode.workspace
.getConfiguration("rubyLsp")
.get<string | undefined>("rubyExecutablePath");

if (globalRubyPath) {
await this.runActivation(
new Shadowenv(this.workspaceFolder, this.outputChannel),
new None(this.workspaceFolder, this.outputChannel, globalRubyPath),
);
break;
} else {
this._error = true;

// When running tests, we need to throw the error or else activation may silently fail and it's very difficult
// to debug
if (this.context.extensionMode === vscode.ExtensionMode.Test) {
throw error;
}

await this.handleRubyError(error.message);
}
}
}

if (!this.error) {
this.fetchRubyVersionInfo();
await this.setupBundlePath();
this._error = false;
} catch (error: any) {
this._error = true;
}
}

// When running tests, we need to throw the error or else activation may silently fail and it's very difficult to
// debug
if (this.context.extensionMode === vscode.ExtensionMode.Test) {
throw error;
}
async manuallySelectRuby() {
const manualSelection = await vscode.window.showInformationMessage(
"Configure global fallback or workspace specific Ruby?",
"global",
"workspace",
"clear previous workspace selection",
);

await vscode.window.showErrorMessage(
`Failed to activate ${this.versionManager.identifier} environment: ${error.message}`,
if (!manualSelection) {
return;
}

if (manualSelection === "clear previous workspace selection") {
await this.context.workspaceState.update(
`rubyLsp.workspaceRubyPath.${this.workspaceFolder.name}`,
undefined,
);
return this.activateRuby();
}

const selection = await vscode.window.showOpenDialog({
title: `Select Ruby binary path for ${manualSelection} configuration`,
openLabel: "Select Ruby binary",
canSelectMany: false,
});

if (!selection) {
return;
}

const selectedPath = selection[0].fsPath;

if (manualSelection === "global") {
await vscode.workspace
.getConfiguration("rubyLsp")
.update("rubyExecutablePath", selectedPath, true);
} else {
// We must update the cached Ruby path for this workspace if the user decided to change it
await this.context.workspaceState.update(
`rubyLsp.workspaceRubyPath.${this.workspaceFolder.name}`,
selectedPath,
);
}

return this.activateRuby();
}

private async runActivation(manager: VersionManager) {
Expand Down Expand Up @@ -230,6 +253,56 @@ export class Ruby implements RubyInterface {
delete env.DEBUG;
}

private async runManagerActivation() {
switch (this.versionManager.identifier) {
case ManagerIdentifier.Asdf:
await this.runActivation(
new Asdf(this.workspaceFolder, this.outputChannel),
);
break;
case ManagerIdentifier.Chruby:
await this.runActivation(
new Chruby(this.workspaceFolder, this.outputChannel),
);
break;
case ManagerIdentifier.Rbenv:
await this.runActivation(
new Rbenv(this.workspaceFolder, this.outputChannel),
);
break;
case ManagerIdentifier.Rvm:
await this.runActivation(
new Rvm(this.workspaceFolder, this.outputChannel),
);
break;
case ManagerIdentifier.Mise:
await this.runActivation(
new Mise(this.workspaceFolder, this.outputChannel),
);
break;
case ManagerIdentifier.RubyInstaller:
await this.runActivation(
new RubyInstaller(this.workspaceFolder, this.outputChannel),
);
break;
case ManagerIdentifier.Custom:
await this.runActivation(
new Custom(this.workspaceFolder, this.outputChannel),
);
break;
case ManagerIdentifier.None:
await this.runActivation(
new None(this.workspaceFolder, this.outputChannel),
);
break;
default:
await this.runActivation(
new Shadowenv(this.workspaceFolder, this.outputChannel),
);
break;
}
}

private async 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
Expand Down Expand Up @@ -322,4 +395,25 @@ export class Ruby implements RubyInterface {
return false;
}
}

private async handleRubyError(message: string) {
const answer = await vscode.window.showErrorMessage(
`Automatic Ruby environment activation with ${this.versionManager.identifier} failed: ${message}`,
"Retry",
"Select Ruby manually",
);

// If the user doesn't answer anything, we can just return. The error property was already set to true and we won't
// try to launch the LSP
if (!answer) {
return;
}

// For retrying, reload the entire window to get rid of any state
if (answer === "Retry") {
await vscode.commands.executeCommand("workbench.action.reloadWindow");
}

return this.manuallySelectRuby();
}
}
23 changes: 20 additions & 3 deletions vscode/src/ruby/none.ts
@@ -1,5 +1,8 @@
/* eslint-disable no-process-env */
import * as vscode from "vscode";

import { asyncExec } from "../common";
import { WorkspaceChannel } from "../workspaceChannel";

import { VersionManager, ActivationResult } from "./versionManager";

Expand All @@ -12,13 +15,27 @@ import { VersionManager, ActivationResult } from "./versionManager";
// If you don't have Ruby automatically available in your PATH and are not using a version manager, look into
// configuring custom Ruby activation
export class None extends VersionManager {
private readonly rubyPath: string;

constructor(
workspaceFolder: vscode.WorkspaceFolder,
outputChannel: WorkspaceChannel,
rubyPath?: string,
) {
super(workspaceFolder, outputChannel);
this.rubyPath = rubyPath ?? "ruby";
}

async activate(): Promise<ActivationResult> {
const activationScript =
"STDERR.print({ env: ENV.to_h, yjit: !!defined?(RubyVM::YJIT), version: RUBY_VERSION }.to_json)";

const result = await asyncExec(`ruby -W0 -rjson -e '${activationScript}'`, {
cwd: this.bundleUri.fsPath,
});
const result = await asyncExec(
`${this.rubyPath} -W0 -rjson -e '${activationScript}'`,
{
cwd: this.bundleUri.fsPath,
},
);

const parsedResult = JSON.parse(result.stderr);
return {
Expand Down
51 changes: 36 additions & 15 deletions vscode/src/rubyLsp.ts
Expand Up @@ -285,22 +285,43 @@ export class RubyLsp {
vscode.commands.registerCommand(
Command.SelectVersionManager,
async () => {
const configuration = vscode.workspace.getConfiguration("rubyLsp");
const managerConfig =
configuration.get<ManagerConfiguration>("rubyVersionManager")!;
const options = Object.values(ManagerIdentifier);
const manager = (await vscode.window.showQuickPick(options, {
placeHolder: `Current: ${managerConfig.identifier}`,
})) as ManagerIdentifier | undefined;

if (manager !== undefined) {
managerConfig.identifier = manager;
await configuration.update(
"rubyVersionManager",
managerConfig,
true,
);
const answer = await vscode.window.showQuickPick(
["Change version manager", "Change manual Ruby configuration"],
{ placeHolder: "What would you like to do?" },
);

if (!answer) {
return;
}

if (answer === "Change version manager") {
const configuration = vscode.workspace.getConfiguration("rubyLsp");
const managerConfig =
configuration.get<ManagerConfiguration>("rubyVersionManager")!;
const options = Object.values(ManagerIdentifier);
const manager = (await vscode.window.showQuickPick(options, {
placeHolder: `Current: ${managerConfig.identifier}`,
})) as ManagerIdentifier | undefined;

if (manager !== undefined) {
managerConfig.identifier = manager;
await configuration.update(
"rubyVersionManager",
managerConfig,
true,
);
}

return;
}

const workspace = await this.showWorkspacePick();

if (!workspace) {
return;
}

await workspace.ruby.manuallySelectRuby();
},
),
vscode.commands.registerCommand(
Expand Down
2 changes: 1 addition & 1 deletion vscode/src/status.ts
Expand Up @@ -36,7 +36,7 @@ export class RubyVersionStatus extends StatusItem {

this.item.name = "Ruby LSP Status";
this.item.command = {
title: "Change version manager",
title: "Configure",
command: Command.SelectVersionManager,
};

Expand Down
8 changes: 7 additions & 1 deletion vscode/src/test/suite/debugger.test.ts
Expand Up @@ -190,7 +190,13 @@ suite("Debugger", () => {
'source "https://rubygems.org"\ngem "debug"',
);

const context = { subscriptions: [] } as unknown as vscode.ExtensionContext;
const context = {
subscriptions: [],
workspaceState: {
get: () => undefined,
update: () => undefined,
},
} as unknown as vscode.ExtensionContext;
const outputChannel = new WorkspaceChannel("fake", LOG_CHANNEL);
const workspaceFolder: vscode.WorkspaceFolder = {
uri: vscode.Uri.file(tmpPath),
Expand Down

0 comments on commit ce9cdf6

Please sign in to comment.