diff --git a/vscode/src/client.ts b/vscode/src/client.ts index d7cf40784..a676f2f1e 100644 --- a/vscode/src/client.ts +++ b/vscode/src/client.ts @@ -30,7 +30,15 @@ import { ErrorCodes, } from "vscode-languageclient/node"; -import { LSP_NAME, ClientInterface, Addon, SUPPORTED_LANGUAGE_IDS, FEATURE_FLAGS, featureEnabled } from "./common"; +import { + LSP_NAME, + ClientInterface, + Addon, + SUPPORTED_LANGUAGE_IDS, + FEATURE_FLAGS, + featureEnabled, + isWindows, +} from "./common"; import { Ruby } from "./ruby"; import { WorkspaceChannel } from "./workspaceChannel"; @@ -121,7 +129,7 @@ function getLspExecutables(workspaceFolder: vscode.WorkspaceFolder, env: NodeJS. } else { const workspacePath = workspaceFolder.uri.fsPath; const command = - path.basename(workspacePath) === "ruby-lsp" && os.platform() !== "win32" + path.basename(workspacePath) === "ruby-lsp" && !isWindows() ? path.join(workspacePath, "exe", "ruby-lsp") : "ruby-lsp"; diff --git a/vscode/src/common.ts b/vscode/src/common.ts index e85f376db..1de9c64c1 100644 --- a/vscode/src/common.ts +++ b/vscode/src/common.ts @@ -1,6 +1,7 @@ import { exec } from "child_process"; import { createHash } from "crypto"; import { promisify } from "util"; +import os from "os"; import * as vscode from "vscode"; import { State } from "vscode-languageclient"; @@ -150,3 +151,8 @@ export function featureEnabled(feature: keyof typeof FEATURE_FLAGS): boolean { export function pathToUri(basePath: string, ...segments: string[]): vscode.Uri { return vscode.Uri.joinPath(vscode.Uri.file(basePath), ...segments); } + +// Helper to check if running on Windows platform +export function isWindows(): boolean { + return os.platform() === "win32"; +} diff --git a/vscode/src/debugger.ts b/vscode/src/debugger.ts index 6b2ed7d35..17ef07ef0 100644 --- a/vscode/src/debugger.ts +++ b/vscode/src/debugger.ts @@ -1,10 +1,9 @@ import net from "net"; -import os from "os"; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import * as vscode from "vscode"; -import { LOG_CHANNEL, asyncExec } from "./common"; +import { LOG_CHANNEL, asyncExec, isWindows } from "./common"; import { Workspace } from "./workspace"; class TerminalLogger { @@ -220,7 +219,7 @@ export class Debugger implements vscode.DebugAdapterDescriptorFactory, vscode.De const configuration = session.configuration; const workspaceFolder = configuration.targetFolder; const cwd = workspaceFolder.path; - const port = os.platform() === "win32" ? await this.availablePort() : undefined; + const port = isWindows() ? await this.availablePort() : undefined; return new Promise((resolve, reject) => { const args = ["exec", "rdbg"]; diff --git a/vscode/src/rails.ts b/vscode/src/rails.ts index a7c9b9549..a066d68d8 100644 --- a/vscode/src/rails.ts +++ b/vscode/src/rails.ts @@ -1,10 +1,9 @@ -import os from "os"; - import * as vscode from "vscode"; import { Workspace } from "./workspace"; +import { isWindows } from "./common"; -const BASE_COMMAND = os.platform() === "win32" ? "ruby bin/rails" : "bin/rails"; +const BASE_COMMAND = isWindows() ? "ruby bin/rails" : "bin/rails"; export class Rails { private readonly showWorkspacePick: () => Promise; diff --git a/vscode/src/ruby.ts b/vscode/src/ruby.ts index c7658c7e2..f8a7b1a9b 100644 --- a/vscode/src/ruby.ts +++ b/vscode/src/ruby.ts @@ -4,7 +4,8 @@ import * as vscode from "vscode"; import { RubyInterface } from "./common"; import { WorkspaceChannel } from "./workspaceChannel"; -import { Shadowenv, UntrustedWorkspaceError } from "./ruby/shadowenv"; +import { Shadowenv } from "./ruby/shadowenv"; +import { UntrustedWorkspaceError } from "./ruby/errors"; import { Chruby } from "./ruby/chruby"; import { VersionManager, DetectionResult } from "./ruby/versionManager"; import { Mise } from "./ruby/mise"; diff --git a/vscode/src/ruby/asdf.ts b/vscode/src/ruby/asdf.ts index f8b970bb9..b8a03449e 100644 --- a/vscode/src/ruby/asdf.ts +++ b/vscode/src/ruby/asdf.ts @@ -6,6 +6,7 @@ import * as vscode from "vscode"; import { VersionManager, ActivationResult, DetectionResult } from "./versionManager"; import { WorkspaceChannel } from "../workspaceChannel"; import { pathToUri } from "../common"; +import { ExecutableNotFoundError } from "./errors"; // A tool to manage multiple runtime versions with a single CLI tool // @@ -75,12 +76,10 @@ export class Asdf extends VersionManager { } else if (result.type === "semantic") { // Use ASDF from PATH } else { - throw new Error( - `Could not find ASDF installation. Searched in ${[ - ...Asdf.getPossibleExecutablePaths(), - ...Asdf.getPossibleScriptPaths(), - ].join(", ")}`, - ); + throw new ExecutableNotFoundError("asdf", [ + ...Asdf.getPossibleExecutablePaths().map((uri) => uri.fsPath), + ...Asdf.getPossibleScriptPaths().map((uri) => uri.fsPath), + ]); } } @@ -115,12 +114,11 @@ export class Asdf extends VersionManager { const configuredPath = vscode.Uri.file(asdfPath); - try { - await vscode.workspace.fs.stat(configuredPath); - this.outputChannel.info(`Using configured ASDF executable path: ${asdfPath}`); - return configuredPath.fsPath; - } catch (_error: any) { - throw new Error(`ASDF executable configured as ${configuredPath.fsPath}, but that file doesn't exist`); + if (!(await VersionManager.pathExists(configuredPath))) { + throw new ExecutableNotFoundError("asdf", [configuredPath.fsPath], configuredPath.fsPath); } + + this.outputChannel.info(`Using configured ASDF executable path: ${asdfPath}`); + return configuredPath.fsPath; } } diff --git a/vscode/src/ruby/chruby.ts b/vscode/src/ruby/chruby.ts index c0f2daa21..cc1658797 100644 --- a/vscode/src/ruby/chruby.ts +++ b/vscode/src/ruby/chruby.ts @@ -5,6 +5,12 @@ import * as vscode from "vscode"; import { WorkspaceChannel } from "../workspaceChannel"; import { pathToUri } from "../common"; +import { + RubyVersionFileError, + RubyInstallationNotFoundError, + RubyVersionFileNotFoundError, + ActivationCancellationError, +} from "./errors"; import { ActivationResult, VersionManager, ACTIVATION_SEPARATOR, DetectionResult } from "./versionManager"; @@ -13,7 +19,8 @@ interface RubyVersion { version: string; } -class RubyActivationCancellationError extends Error {} +// Delay before attempting fallback Ruby activation (in milliseconds) +const FALLBACK_DELAY_MS = 10000; // A tool to change the current Ruby version // Learn more: https://github.com/postmodern/chruby @@ -66,8 +73,8 @@ export class Chruby extends VersionManager { versionInfo = fallback.rubyVersion; rubyUri = fallback.uri; } - } catch (error: any) { - if (error instanceof RubyActivationCancellationError) { + } catch (error: unknown) { + if (error instanceof ActivationCancellationError) { // Try to re-activate if the user has configured a fallback during cancellation return this.activate(); } @@ -91,8 +98,8 @@ export class Chruby extends VersionManager { versionInfo = fallback.rubyVersion; rubyUri = fallback.uri; } - } catch (error: any) { - if (error instanceof RubyActivationCancellationError) { + } catch (error: unknown) { + if (error instanceof ActivationCancellationError) { // Try to re-activate if the user has configured a fallback during cancellation return this.activate(); } @@ -127,7 +134,16 @@ export class Chruby extends VersionManager { return process.env.PATH; } - // Returns the full URI to the Ruby executable + /** + * Finds the URI to the Ruby executable for a given Ruby version. + * + * This method searches through configured Ruby installation directories + * (like ~/.rubies and /opt/rubies) to find a matching Ruby installation + * based on the provided version information. + * + * @param rubyVersion - The Ruby version to search for, including optional engine name + * @returns URI to the Ruby executable if found, undefined otherwise + */ protected async findRubyUri(rubyVersion: RubyVersion): Promise { const possibleVersionNames = rubyVersion.engine ? [`${rubyVersion.engine}-${rubyVersion.version}`, rubyVersion.version] @@ -140,7 +156,7 @@ export class Chruby extends VersionManager { directories = (await vscode.workspace.fs.readDirectory(uri)).sort((left, right) => right[0].localeCompare(left[0]), ); - } catch (_error: any) { + } catch (_error: unknown) { // If the directory doesn't exist, keep searching this.outputChannel.debug(`Tried searching for Ruby installation in ${uri.fsPath} but it doesn't exist`); continue; @@ -150,7 +166,7 @@ export class Chruby extends VersionManager { const targetDirectory = directories.find(([name]) => name.startsWith(versionName)); if (targetDirectory) { - return vscode.Uri.joinPath(uri, targetDirectory[0], "bin", "ruby"); + return this.rubyExecutableUri(uri, targetDirectory[0]); } } } @@ -158,7 +174,16 @@ export class Chruby extends VersionManager { return undefined; } - // Run the activation script using the Ruby installation we found so that we can discover gem paths + /** + * Runs the activation script to discover Ruby environment information. + * + * This executes a Ruby script (chruby_activation.rb) to determine gem paths, + * YJIT availability, and actual Ruby version for the specified installation. + * + * @param rubyExecutableUri - URI to the Ruby executable to activate + * @param rubyVersion - The Ruby version information + * @returns Object containing default gems path, gem home, YJIT status, and version + */ protected async runActivationScript( rubyExecutableUri: vscode.Uri, rubyVersion: RubyVersion, @@ -179,11 +204,29 @@ export class Chruby extends VersionManager { return { defaultGems, gemHome, yjit: yjit === "true", version }; } + /** + * Finds the closest Ruby installation when exact version match is not available. + * + * This method implements a fallback strategy by: + * 1. Searching for Ruby installations with the same major version + * 2. Sorting to find the version with minimal minor version distance + * 3. For equal distances, preferring higher patch versions + * + * For example, if 3.2.0 is requested but not found: + * - 3.2.1 or 3.2.5 would be preferred (same minor, higher patch) + * - Then 3.1.x or 3.3.x (minor distance of 1) + * - Then 3.0.x or 3.4.x (minor distance of 2) + * + * @param rubyVersion - The requested Ruby version + * @returns Object with URI and version info of the closest installation + * @throws RubyInstallationNotFoundError if no Ruby with matching major version exists + */ private async findClosestRubyInstallation(rubyVersion: RubyVersion): Promise<{ uri: vscode.Uri; rubyVersion: RubyVersion; }> { const [major, minor, _patch] = rubyVersion.version.split("."); + const targetMinor = Number(minor); const directories: { uri: vscode.Uri; rubyVersion: RubyVersion }[] = []; for (const uri of this.rubyInstallationUris) { @@ -195,7 +238,7 @@ export class Chruby extends VersionManager { if (match?.groups && match.groups.version.startsWith(major)) { directories.push({ - uri: vscode.Uri.joinPath(uri, name, "bin", "ruby"), + uri: this.rubyExecutableUri(uri, name), rubyVersion: { engine: match.groups.engine, version: match.groups.version, @@ -203,38 +246,59 @@ export class Chruby extends VersionManager { }); } }); - } catch (_error: any) { + } catch (_error: unknown) { // If the directory doesn't exist, keep searching this.outputChannel.debug(`Tried searching for Ruby installation in ${uri.fsPath} but it doesn't exist`); continue; } } - // Sort the directories based on the difference between the minor version and the requested minor version. On - // conflicts, we use the patch version to break the tie. If there's no distance, we prefer the higher patch version + // Sort to find the closest match: + // 1. Prefer versions with minimal minor version distance + // 2. For equal minor version distance, prefer higher patch version const closest = directories.sort((left, right) => { - const leftVersion = left.rubyVersion.version.split("."); - const rightVersion = right.rubyVersion.version.split("."); + const leftParts = left.rubyVersion.version.split("."); + const rightParts = right.rubyVersion.version.split("."); - const leftDiff = Math.abs(Number(leftVersion[1]) - Number(minor)); - const rightDiff = Math.abs(Number(rightVersion[1]) - Number(minor)); + const leftMinor = Number(leftParts[1]); + const rightMinor = Number(rightParts[1]); - // If the distance to minor version is the same, prefer higher patch number - if (leftDiff === rightDiff) { - return Number(rightVersion[2] || 0) - Number(leftVersion[2] || 0); + const leftMinorDistance = Math.abs(leftMinor - targetMinor); + const rightMinorDistance = Math.abs(rightMinor - targetMinor); + + // Primary sort: prefer closer minor version + if (leftMinorDistance !== rightMinorDistance) { + return leftMinorDistance - rightMinorDistance; } - return leftDiff - rightDiff; + // Secondary sort: for same minor distance, prefer higher patch + const leftPatch = Number(leftParts[2] || 0); + const rightPatch = Number(rightParts[2] || 0); + return rightPatch - leftPatch; })[0]; if (closest) { return closest; } - throw new Error("Cannot find any Ruby installations"); + throw new RubyInstallationNotFoundError("chruby"); } - // Returns the Ruby version information including version and engine. E.g.: ruby-3.3.0, truffleruby-21.3.0 + /** + * Discovers the Ruby version from .ruby-version files. + * + * This method walks up the directory tree starting from the workspace root, + * searching for a .ruby-version file. The first file found is parsed and + * validated using a regex pattern to extract engine (optional) and version. + * + * Supported formats: + * - 3.3.0 (just version) + * - ruby-3.3.0 (engine-version) + * - truffleruby-21.3.0 (alternative engine) + * + * @returns Ruby version information if found, undefined if no .ruby-version exists + * @throws RubyVersionFileError if .ruby-version is empty or has invalid format + */ private async discoverRubyVersion(): Promise { let uri = this.bundleUri; const root = path.parse(uri.fsPath).root; @@ -253,15 +317,13 @@ export class Chruby extends VersionManager { } if (version === "") { - throw new Error(`Ruby version file ${rubyVersionUri.fsPath} is empty`); + throw new RubyVersionFileError(rubyVersionUri.fsPath, "empty", version); } const match = /((?[A-Za-z]+)-)?(?\d+\.\d+(\.\d+)?(-[A-Za-z0-9]+)?)/.exec(version); if (!match?.groups) { - throw new Error( - `Ruby version file ${rubyVersionUri.fsPath} contains invalid format. Expected (engine-)?version, got ${version}`, - ); + throw new RubyVersionFileError(rubyVersionUri.fsPath, "invalid_format", version); } this.outputChannel.info(`Discovered Ruby version ${version} from ${rubyVersionUri.fsPath}`); @@ -281,7 +343,7 @@ export class Chruby extends VersionManager { try { gemfileContents = await vscode.workspace.fs.readFile(vscode.Uri.joinPath(this.workspaceFolder.uri, "Gemfile")); - } catch (_error: any) { + } catch (_error: unknown) { // The Gemfile doesn't exist } @@ -301,7 +363,7 @@ export class Chruby extends VersionManager { // If they don't cancel, we wait 10 seconds before falling back so that they are aware of what's happening await new Promise((resolve) => { - setTimeout(resolve, 10000); + setTimeout(resolve, FALLBACK_DELAY_MS); // If the user cancels the fallback, resolve immediately so that they don't have to wait 10 seconds token.onCancellationRequested(() => { @@ -313,7 +375,7 @@ export class Chruby extends VersionManager { await this.handleCancelledFallback(errorFn); // We throw this error to be able to catch and re-run activation after the user has configured a fallback - throw new RubyActivationCancellationError(); + throw new ActivationCancellationError("chruby"); } return fallbackFn(); @@ -356,7 +418,7 @@ export class Chruby extends VersionManager { label: directory[0], }); }); - } catch (_error: any) { + } catch (_error: unknown) { continue; } } @@ -389,6 +451,16 @@ export class Chruby extends VersionManager { ); } + /** + * Finds a fallback Ruby installation when no .ruby-version file exists. + * + * This searches through configured Ruby installation directories and + * returns the first valid Ruby installation found (sorted by version + * descending, so the latest version is preferred). + * + * @returns Object with URI and version info of the fallback installation + * @throws RubyInstallationNotFoundError if no Ruby installation exists + */ private async findFallbackRuby(): Promise<{ uri: vscode.Uri; rubyVersion: RubyVersion; @@ -416,31 +488,28 @@ export class Chruby extends VersionManager { if (targetDirectory && groups) { return { - uri: vscode.Uri.joinPath(uri, targetDirectory[0], "bin", "ruby"), + uri: this.rubyExecutableUri(uri, targetDirectory[0]), rubyVersion: { engine: groups.engine, version: groups.version, }, }; } - } catch (_error: any) { + } catch (_error: unknown) { // If the directory doesn't exist, keep searching this.outputChannel.debug(`Tried searching for Ruby installation in ${uri.fsPath} but it doesn't exist`); continue; } } - throw new Error("Cannot find any Ruby installations"); + throw new RubyInstallationNotFoundError("chruby"); } private missingRubyError(version: string) { - return new Error(`Cannot find Ruby installation for version ${version}`); + return new RubyInstallationNotFoundError("chruby", version); } private rubyVersionError() { - return new Error( - `Cannot find .ruby-version file. Please specify the Ruby version in a - .ruby-version either in ${this.bundleUri.fsPath} or in a parent directory`, - ); + return new RubyVersionFileNotFoundError("chruby", this.bundleUri.fsPath); } } diff --git a/vscode/src/ruby/custom.ts b/vscode/src/ruby/custom.ts index df86d9426..8e8831ca3 100644 --- a/vscode/src/ruby/custom.ts +++ b/vscode/src/ruby/custom.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode"; import { VersionManager, ActivationResult, DetectionResult } from "./versionManager"; +import { MissingConfigurationError } from "./errors"; // Custom // @@ -32,10 +33,7 @@ export class Custom extends VersionManager { const customCommand: string | undefined = configuration.get("customRubyCommand"); if (customCommand === undefined) { - throw new Error( - "The customRubyCommand configuration must be set when 'custom' is selected as the version manager. \ - See the [README](https://shopify.github.io/ruby-lsp/version-managers.html) for instructions.", - ); + throw new MissingConfigurationError("custom", "rubyLsp.customRubyCommand"); } return customCommand; diff --git a/vscode/src/ruby/errors.ts b/vscode/src/ruby/errors.ts new file mode 100644 index 000000000..d3b103c47 --- /dev/null +++ b/vscode/src/ruby/errors.ts @@ -0,0 +1,161 @@ +// Custom error types for Ruby version manager operations + +/** + * Base class for all Ruby version manager errors. + * Provides structured error information including the version manager name and error code. + */ +export abstract class RubyVersionManagerError extends Error { + public readonly versionManager: string; + public readonly errorCode: string; + + constructor(message: string, versionManager: string, errorCode: string) { + super(message); + this.name = this.constructor.name; + this.versionManager = versionManager; + this.errorCode = errorCode; + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + } +} + +/** + * Thrown when a version manager executable cannot be found at expected locations. + */ +export class ExecutableNotFoundError extends RubyVersionManagerError { + public readonly searchedPaths: string[]; + public readonly configuredPath?: string; + + constructor(versionManager: string, searchedPaths: string[], configuredPath?: string) { + const message = configuredPath + ? `${versionManager} executable configured as ${configuredPath}, but that file doesn't exist` + : `Cannot find ${versionManager} installation. Searched in ${searchedPaths.join(", ")}`; + super(message, versionManager, "EXECUTABLE_NOT_FOUND"); + this.searchedPaths = searchedPaths; + this.configuredPath = configuredPath; + } +} + +/** + * Thrown when required configuration is missing for a version manager. + */ +export class MissingConfigurationError extends RubyVersionManagerError { + public readonly configKey: string; + + constructor(versionManager: string, configKey: string) { + super( + `The ${configKey} configuration must be set when '${versionManager}' is selected as the version manager. See the [README](https://shopify.github.io/ruby-lsp/version-managers.html) for instructions.`, + versionManager, + "MISSING_CONFIGURATION", + ); + this.configKey = configKey; + } +} + +/** + * Thrown when a Ruby installation cannot be found for the requested version. + */ +export class RubyInstallationNotFoundError extends RubyVersionManagerError { + public readonly requestedVersion?: string; + public readonly searchedPaths?: string[]; + + constructor(versionManager: string, requestedVersion?: string, searchedPaths?: string[]) { + let message: string; + if (searchedPaths && searchedPaths.length > 0) { + message = requestedVersion + ? `Cannot find Ruby installation for version ${requestedVersion}. Searched in ${searchedPaths.join(", ")}` + : `Cannot find any Ruby installations. Searched in ${searchedPaths.join(", ")}`; + } else { + message = requestedVersion + ? `Cannot find Ruby installation for version ${requestedVersion}` + : "Cannot find any Ruby installations"; + } + super(message, versionManager, "RUBY_NOT_FOUND"); + this.requestedVersion = requestedVersion; + this.searchedPaths = searchedPaths; + } +} + +/** + * Thrown when a version manager's required directory structure is not found. + */ +export class VersionManagerDirectoryNotFoundError extends RubyVersionManagerError { + public readonly directoryName: string; + + constructor(versionManager: string, directoryName: string) { + super( + `The Ruby LSP version manager is configured to be ${versionManager}, but no ${directoryName} directory was found in the workspace`, + versionManager, + "MANAGER_DIR_NOT_FOUND", + ); + this.directoryName = directoryName; + } +} + +/** + * Thrown when a .ruby-version file is empty or contains invalid format. + */ +export class RubyVersionFileError extends RubyVersionManagerError { + public readonly filePath: string; + public readonly issue: "empty" | "invalid_format"; + public readonly content?: string; + + constructor(filePath: string, issue: "empty" | "invalid_format", content?: string) { + const message = + issue === "empty" + ? `Ruby version file ${filePath} is empty` + : `Ruby version file ${filePath} contains invalid format. Expected (engine-)?version, got ${content}`; + super(message, "chruby", "VERSION_FILE_ERROR"); + this.filePath = filePath; + this.issue = issue; + this.content = content; + } +} + +/** + * Base class for errors that occur during Ruby environment activation. + */ +export class ActivationError extends RubyVersionManagerError { + public readonly cause?: Error; + + constructor(message: string, versionManager: string, cause?: Error) { + super(message, versionManager, "ACTIVATION_ERROR"); + this.cause = cause; + } +} + +/** + * Thrown when attempting to activate Ruby in an untrusted workspace (e.g., for Shadowenv). + */ +export class UntrustedWorkspaceError extends ActivationError { + constructor(versionManager: string = "shadowenv") { + super("Cannot activate Ruby environment in an untrusted workspace", versionManager); + } +} + +/** + * Thrown when user cancels a Ruby activation fallback operation. + */ +export class ActivationCancellationError extends ActivationError { + constructor(versionManager: string) { + super("Ruby activation was cancelled by user", versionManager); + } +} + +/** + * Thrown when no .ruby-version file can be found in the workspace hierarchy. + */ +export class RubyVersionFileNotFoundError extends RubyVersionManagerError { + public readonly searchedPath: string; + + constructor(versionManager: string, searchedPath: string) { + super( + `Cannot find .ruby-version file. Please specify the Ruby version in a .ruby-version either in ${searchedPath} or in a parent directory`, + versionManager, + "VERSION_FILE_NOT_FOUND", + ); + this.searchedPath = searchedPath; + } +} diff --git a/vscode/src/ruby/mise.ts b/vscode/src/ruby/mise.ts index 90a7d902f..95ed6c91d 100644 --- a/vscode/src/ruby/mise.ts +++ b/vscode/src/ruby/mise.ts @@ -5,6 +5,7 @@ import * as vscode from "vscode"; import { VersionManager, ActivationResult, DetectionResult } from "./versionManager"; import { WorkspaceChannel } from "../workspaceChannel"; import { pathToUri } from "../common"; +import { ExecutableNotFoundError } from "./errors"; // Mise (mise en place) is a manager for dev tools, environment variables and tasks // @@ -72,7 +73,7 @@ export class Mise extends VersionManager { await vscode.workspace.fs.stat(uri); return uri; } catch (_error: any) { - throw new Error(`${managerName} executable configured as ${uri.fsPath}, but that file doesn't exist`); + throw new ExecutableNotFoundError(managerName, [uri.fsPath], uri.fsPath); } } @@ -83,9 +84,9 @@ export class Mise extends VersionManager { } const possiblePaths = constructor.getPossiblePaths(); - throw new Error( - `The Ruby LSP version manager is configured to be ${managerName}, but could not find ${managerName} installation. Searched in - ${possiblePaths.map((p) => p.fsPath).join(", ")}`, + throw new ExecutableNotFoundError( + managerName, + possiblePaths.map((uri) => uri.fsPath), ); } } diff --git a/vscode/src/ruby/rbenv.ts b/vscode/src/ruby/rbenv.ts index d145d4800..1866c7396 100644 --- a/vscode/src/ruby/rbenv.ts +++ b/vscode/src/ruby/rbenv.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode"; import { VersionManager, ActivationResult, DetectionResult } from "./versionManager"; import { WorkspaceChannel } from "../workspaceChannel"; +import { ExecutableNotFoundError } from "./errors"; // Seamlessly manage your app’s Ruby environment with rbenv. // @@ -45,7 +46,7 @@ export class Rbenv extends VersionManager { return path; } catch (_error: any) { - throw new Error(`The Ruby LSP version manager is configured to be rbenv, but ${path} does not exist`); + throw new ExecutableNotFoundError("rbenv", [path], path); } } } diff --git a/vscode/src/ruby/rubyInstaller.ts b/vscode/src/ruby/rubyInstaller.ts index ec538f982..289a86978 100644 --- a/vscode/src/ruby/rubyInstaller.ts +++ b/vscode/src/ruby/rubyInstaller.ts @@ -3,8 +3,9 @@ import os from "os"; import * as vscode from "vscode"; import { Chruby } from "./chruby"; -import { pathToUri } from "../common"; -import { DetectionResult } from "./versionManager"; +import { pathToUri, isWindows } from "../common"; +import { RubyInstallationNotFoundError } from "./errors"; +import { DetectionResult, VersionManager } from "./versionManager"; interface RubyVersion { engine?: string; @@ -23,7 +24,7 @@ export class RubyInstaller extends Chruby { _workspaceFolder: vscode.WorkspaceFolder, _outputChannel: vscode.LogOutputChannel, ): Promise { - return os.platform() === "win32" ? { type: "semantic", marker: "RubyInstaller" } : { type: "none" }; + return isWindows() ? { type: "semantic", marker: "RubyInstaller" } : { type: "none" }; } // Environment variables are case sensitive on Windows when we access them through NodeJS. We need to ensure that @@ -42,17 +43,15 @@ export class RubyInstaller extends Chruby { ]; for (const installationUri of possibleInstallationUris) { - try { - await vscode.workspace.fs.stat(installationUri); - return vscode.Uri.joinPath(installationUri, "bin", "ruby"); - } catch (_error: any) { - // Continue searching + if (await VersionManager.pathExists(installationUri)) { + return this.rubyExecutableUri(installationUri); } } - throw new Error( - `Cannot find installation directory for Ruby version ${rubyVersion.version}.\ - Searched in ${possibleInstallationUris.map((uri) => uri.fsPath).join(", ")}`, + throw new RubyInstallationNotFoundError( + "rubyInstaller", + rubyVersion.version, + possibleInstallationUris.map((uri) => uri.fsPath), ); } diff --git a/vscode/src/ruby/rvm.ts b/vscode/src/ruby/rvm.ts index b8bd24709..b3d7a5f3e 100644 --- a/vscode/src/ruby/rvm.ts +++ b/vscode/src/ruby/rvm.ts @@ -5,6 +5,7 @@ import * as vscode from "vscode"; import { ActivationResult, VersionManager, DetectionResult } from "./versionManager"; import { WorkspaceChannel } from "../workspaceChannel"; import { pathToUri } from "../common"; +import { ExecutableNotFoundError } from "./errors"; // Ruby enVironment Manager. It manages Ruby application environments and enables switching between them. // Learn more: @@ -53,8 +54,9 @@ export class Rvm extends VersionManager { } } - throw new Error( - `Cannot find RVM installation directory. Searched in ${possiblePaths.map((uri) => uri.fsPath).join(",")}`, + throw new ExecutableNotFoundError( + "rvm", + possiblePaths.map((uri) => uri.fsPath), ); } } diff --git a/vscode/src/ruby/shadowenv.ts b/vscode/src/ruby/shadowenv.ts index 6a33ac3ec..e1c61f6ae 100644 --- a/vscode/src/ruby/shadowenv.ts +++ b/vscode/src/ruby/shadowenv.ts @@ -1,6 +1,12 @@ import * as vscode from "vscode"; import { asyncExec } from "../common"; +import { + VersionManagerDirectoryNotFoundError, + UntrustedWorkspaceError, + ExecutableNotFoundError, + ActivationError, +} from "./errors"; import { VersionManager, ActivationResult, DetectionResult } from "./versionManager"; @@ -8,16 +14,10 @@ import { VersionManager, ActivationResult, DetectionResult } from "./versionMana // which Ruby version should be used for each project, in addition to other customizations such as GEM_HOME. // // Learn more: https://github.com/Shopify/shadowenv -export class UntrustedWorkspaceError extends Error {} export class Shadowenv extends VersionManager { private static async shadowenvDirExists(workspaceUri: vscode.Uri): Promise { - try { - await vscode.workspace.fs.stat(vscode.Uri.joinPath(workspaceUri, ".shadowenv.d")); - return true; - } catch (_error: any) { - return false; - } + return VersionManager.pathExists(vscode.Uri.joinPath(workspaceUri, ".shadowenv.d")); } static async detect( @@ -34,10 +34,7 @@ export class Shadowenv extends VersionManager { async activate(): Promise { const exists = await Shadowenv.shadowenvDirExists(this.bundleUri); if (!exists) { - throw new Error( - "The Ruby LSP version manager is configured to be shadowenv, \ - but no .shadowenv.d directory was found in the workspace", - ); + throw new VersionManagerDirectoryNotFoundError("shadowenv", ".shadowenv.d"); } const shadowenvExec = await this.findExec([vscode.Uri.file("/opt/homebrew/bin")], "shadowenv"); @@ -55,7 +52,7 @@ export class Shadowenv extends VersionManager { version: parsedResult.version, gemPath: parsedResult.gemPath, }; - } catch (error: any) { + } catch (error: unknown) { const err = error as Error; // If the workspace is untrusted, offer to trust it for the user if (err.message.includes("untrusted shadowenv program")) { @@ -71,21 +68,17 @@ export class Shadowenv extends VersionManager { return this.activate(); } - throw new UntrustedWorkspaceError("Cannot activate Ruby environment in an untrusted workspace"); + throw new UntrustedWorkspaceError("shadowenv"); } try { await asyncExec("shadowenv --version"); - } catch (_error: any) { - throw new Error( - `Shadowenv executable not found. Ensure it is installed and available in the PATH. - This error may happen if your shell configuration is failing to be sourced from the editor or if - another extension is mutating the process PATH.`, - ); + } catch (_error: unknown) { + throw new ExecutableNotFoundError("shadowenv", ["PATH"]); } // If it failed for some other reason, present the error to the user - throw new Error(`Failed to activate Ruby environment with Shadowenv: ${error.message}`); + throw new ActivationError(`Failed to activate Ruby environment with Shadowenv: ${err.message}`, "shadowenv", err); } } } diff --git a/vscode/src/ruby/versionManager.ts b/vscode/src/ruby/versionManager.ts index 353db6dac..9e3d0cdd8 100644 --- a/vscode/src/ruby/versionManager.ts +++ b/vscode/src/ruby/versionManager.ts @@ -1,10 +1,9 @@ import path from "path"; -import os from "os"; import * as vscode from "vscode"; import { WorkspaceChannel } from "../workspaceChannel"; -import { asyncExec } from "../common"; +import { asyncExec, isWindows } from "../common"; export interface ActivationResult { env: NodeJS.ProcessEnv; @@ -21,6 +20,9 @@ export type DetectionResult = // Changes to either one of these values have to be synchronized with a corresponding update in `activation.rb` export const ACTIVATION_SEPARATOR = "RUBY_LSP_ACTIVATION_SEPARATOR"; + +// Timeout for tool detection commands (in milliseconds) +const TOOL_DETECTION_TIMEOUT_MS = 1000; export const VALUE_SEPARATOR = "RUBY_LSP_VS"; export const FIELD_SEPARATOR = "RUBY_LSP_FS"; @@ -55,25 +57,63 @@ export abstract class VersionManager { : workspaceFolder.uri; } - // Activate the Ruby environment for the version manager, returning all of the necessary information to boot the - // language server + /** + * Activates the Ruby environment for this version manager. + * + * Implementations should discover the Ruby version, locate the Ruby installation, + * and return all necessary environment variables and metadata to boot the Ruby LSP. + * + * @returns Activation result with environment variables, YJIT status, version, and gem paths + * @throws Error if Ruby cannot be activated + */ abstract activate(): Promise; - // Finds the first existing path from a list of possible paths + /** + * Finds the first existing path from a list of possible paths. + * + * This helper iterates through paths in order and returns the first one + * that exists in the filesystem, or undefined if none exist. + * + * @param paths - Array of URIs to check + * @returns First existing URI or undefined if none exist + */ protected static async findFirst(paths: vscode.Uri[]): Promise { for (const possiblePath of paths) { - try { - await vscode.workspace.fs.stat(possiblePath); + if (await this.pathExists(possiblePath)) { return possiblePath; - } catch (_error: any) { - // Continue looking } } return undefined; } - // Checks if a tool exists by running `tool --version` + /** + * Checks if a path exists in the filesystem. + * + * @param uri - The URI to check for existence + * @returns true if the path exists, false otherwise + */ + protected static async pathExists(uri: vscode.Uri): Promise { + try { + await vscode.workspace.fs.stat(uri); + return true; + } catch (_error: unknown) { + return false; + } + } + + /** + * Checks if a version manager tool exists by running its --version command. + * + * This method attempts to execute `tool --version` within the workspace + * to verify the tool is available on the PATH. The command is run in an + * interactive shell to ensure shell initialization files are sourced. + * + * @param tool - Name of the tool to check (e.g., "chruby", "rbenv") + * @param workspaceFolder - Workspace folder to use as working directory + * @param outputChannel - Channel for logging detection attempts + * @returns true if the tool exists and responds to --version, false otherwise + */ static async toolExists( tool: string, workspaceFolder: vscode.WorkspaceFolder, @@ -87,7 +127,7 @@ export abstract class VersionManager { await asyncExec(command, { cwd: workspaceFolder.uri.fsPath, - timeout: 1000, + timeout: TOOL_DETECTION_TIMEOUT_MS, }); return true; } catch { @@ -95,6 +135,17 @@ export abstract class VersionManager { } } + /** + * Runs the Ruby environment activation script to gather environment information. + * + * This executes the activation.rb script using the specified Ruby executable + * to discover environment variables, gem paths, YJIT status, and version. + * The activation script outputs data in a structured format separated by + * ACTIVATION_SEPARATOR and FIELD_SEPARATOR constants. + * + * @param activatedRuby - Command to run Ruby (e.g., "ruby" or full path) + * @returns Activation result with all environment information + */ protected async runEnvActivationScript(activatedRuby: string): Promise { const activationUri = vscode.Uri.joinPath(this.context.extensionUri, "activation.rb"); @@ -112,15 +163,24 @@ export abstract class VersionManager { }; } - // Runs the given command in the directory for the Bundle, using the user's preferred shell and inheriting the current - // process environment + /** + * Runs a shell command in the bundle directory. + * + * This executes the given command using the user's preferred shell (from vscode.env.shell) + * and inherits the current process environment. The shell is used to ensure version manager + * initialization scripts are sourced. On Windows, no shell is specified to avoid PowerShell + * quoting issues. + * + * @param command - Shell command to execute + * @returns Promise resolving to command output + */ protected runScript(command: string) { let shell: string | undefined; // If the user has configured a default shell, we use that one since they are probably sourcing their version // manager scripts in that shell's configuration files. On Windows, we never set the shell no matter what to ensure // that activation runs on `cmd.exe` and not PowerShell, which avoids complex quoting and escaping issues. - if (vscode.env.shell.length > 0 && os.platform() !== "win32") { + if (vscode.env.shell.length > 0 && !isWindows()) { shell = vscode.env.shell; } @@ -134,8 +194,17 @@ export abstract class VersionManager { }); } - // Tries to find `execName` within the given directories. Prefers the executables found in the given directories over - // finding the executable in the PATH + /** + * Searches for an executable within specified directories. + * + * This method checks each directory for the executable and returns the first + * match found. If not found in any directory, returns the execName itself + * which will attempt to find the executable in the PATH. + * + * @param directories - Array of directory URIs to search + * @param execName - Name of the executable to find + * @returns Full path to executable if found, otherwise the execName itself + */ protected async findExec(directories: vscode.Uri[], execName: string) { for (const uri of directories) { try { @@ -150,4 +219,23 @@ export abstract class VersionManager { return execName; } + + /** + * Constructs a URI for a Ruby executable within a Ruby installation directory. + * + * This helper builds the full path to the ruby executable by combining the + * installation root with an optional version subdirectory and the bin/ruby path. + * + * Examples: + * - rubyExecutableUri(/opt/rubies, "ruby-3.3.0") → /opt/rubies/ruby-3.3.0/bin/ruby + * - rubyExecutableUri(/usr/local) → /usr/local/bin/ruby + * + * @param installationUri - The root directory of the Ruby installation + * @param versionDirectory - Optional subdirectory name (e.g., "ruby-3.3.0") + * @returns URI pointing to the Ruby executable + */ + protected rubyExecutableUri(installationUri: vscode.Uri, versionDirectory?: string): vscode.Uri { + const basePath = versionDirectory ? vscode.Uri.joinPath(installationUri, versionDirectory) : installationUri; + return vscode.Uri.joinPath(basePath, "bin", "ruby"); + } } diff --git a/vscode/src/test/suite/client.test.ts b/vscode/src/test/suite/client.test.ts index e5c966b66..ecd63a7ce 100644 --- a/vscode/src/test/suite/client.test.ts +++ b/vscode/src/test/suite/client.test.ts @@ -33,6 +33,7 @@ import { MAJOR, MINOR } from "../rubyVersion"; import { FAKE_TELEMETRY, FakeLogger } from "./fakeTelemetry"; import { createContext, createRubySymlinks } from "./helpers"; +import { isWindows } from "../../common"; async function launchClient(workspaceUri: vscode.Uri) { const workspaceFolder: vscode.WorkspaceFolder = { @@ -54,7 +55,7 @@ async function launchClient(workspaceUri: vscode.Uri) { createRubySymlinks(); - if (os.platform() === "win32") { + if (isWindows()) { managerConfig = { identifier: ManagerIdentifier.RubyInstaller }; } else { managerConfig = { identifier: ManagerIdentifier.Chruby }; diff --git a/vscode/src/test/suite/debugger.test.ts b/vscode/src/test/suite/debugger.test.ts index af62d2912..2b8703b45 100644 --- a/vscode/src/test/suite/debugger.test.ts +++ b/vscode/src/test/suite/debugger.test.ts @@ -10,7 +10,7 @@ import { Debugger } from "../../debugger"; import { Ruby, ManagerIdentifier } from "../../ruby"; import { Workspace } from "../../workspace"; import { WorkspaceChannel } from "../../workspaceChannel"; -import { LOG_CHANNEL, asyncExec, pathToUri } from "../../common"; +import { LOG_CHANNEL, asyncExec, pathToUri, isWindows } from "../../common"; import { RUBY_VERSION } from "../rubyVersion"; import { FAKE_TELEMETRY } from "./fakeTelemetry"; @@ -167,8 +167,7 @@ suite("Debugger", () => { }); test("Launching the debugger", async () => { - const manager = - os.platform() === "win32" ? { identifier: ManagerIdentifier.None } : { identifier: ManagerIdentifier.Chruby }; + const manager = isWindows() ? { identifier: ManagerIdentifier.None } : { identifier: ManagerIdentifier.Chruby }; if (process.env.CI) { createRubySymlinks(); diff --git a/vscode/src/test/suite/launch.test.ts b/vscode/src/test/suite/launch.test.ts index e2909c68a..eab03a31d 100644 --- a/vscode/src/test/suite/launch.test.ts +++ b/vscode/src/test/suite/launch.test.ts @@ -1,6 +1,5 @@ import assert from "assert"; import path from "path"; -import os from "os"; import * as vscode from "vscode"; import { State } from "vscode-languageclient/node"; @@ -43,7 +42,7 @@ suite("Launch integrations", () => { async function createClient() { const ruby = new Ruby(context, workspaceFolder, outputChannel, FAKE_TELEMETRY); - if (process.env.CI && os.platform() === "win32") { + if (process.env.CI && common.isWindows()) { await ruby.activateRuby({ identifier: ManagerIdentifier.RubyInstaller }); } else if (process.env.CI) { await ruby.activateRuby({ identifier: ManagerIdentifier.Chruby }); diff --git a/vscode/src/test/suite/rails.test.ts b/vscode/src/test/suite/rails.test.ts index 69d66cf84..fdd0d6a81 100644 --- a/vscode/src/test/suite/rails.test.ts +++ b/vscode/src/test/suite/rails.test.ts @@ -1,5 +1,4 @@ import * as assert from "assert"; -import os from "os"; import path from "path"; import * as vscode from "vscode"; @@ -8,8 +7,9 @@ import { beforeEach, afterEach } from "mocha"; import { Rails } from "../../rails"; import { Workspace } from "../../workspace"; +import { isWindows } from "../../common"; -const BASE_COMMAND = os.platform() === "win32" ? "ruby bin/rails" : "bin/rails"; +const BASE_COMMAND = isWindows() ? "ruby bin/rails" : "bin/rails"; suite("Rails", () => { const workspacePath = path.dirname(path.dirname(path.dirname(path.dirname(__dirname)))); diff --git a/vscode/src/test/suite/ruby.test.ts b/vscode/src/test/suite/ruby.test.ts index b53f4b139..5c90e55a7 100644 --- a/vscode/src/test/suite/ruby.test.ts +++ b/vscode/src/test/suite/ruby.test.ts @@ -9,7 +9,8 @@ import { Ruby, ManagerIdentifier } from "../../ruby"; import { WorkspaceChannel } from "../../workspaceChannel"; import { LOG_CHANNEL } from "../../common"; import * as common from "../../common"; -import { Shadowenv, UntrustedWorkspaceError } from "../../ruby/shadowenv"; +import { Shadowenv } from "../../ruby/shadowenv"; +import { UntrustedWorkspaceError } from "../../ruby/errors"; import { ACTIVATION_SEPARATOR, FIELD_SEPARATOR, VALUE_SEPARATOR } from "../../ruby/versionManager"; import { createContext, FakeContext } from "./helpers"; diff --git a/vscode/src/test/suite/ruby/asdf.test.ts b/vscode/src/test/suite/ruby/asdf.test.ts index 99936bf13..7a058e0a1 100644 --- a/vscode/src/test/suite/ruby/asdf.test.ts +++ b/vscode/src/test/suite/ruby/asdf.test.ts @@ -13,7 +13,7 @@ import { ACTIVATION_SEPARATOR, FIELD_SEPARATOR, VALUE_SEPARATOR, VersionManager import { createContext, FakeContext } from "../helpers"; suite("Asdf", () => { - if (os.platform() === "win32") { + if (common.isWindows()) { // eslint-disable-next-line no-console console.log("Skipping Asdf tests on Windows"); return; diff --git a/vscode/src/test/suite/ruby/chruby.test.ts b/vscode/src/test/suite/ruby/chruby.test.ts index 58d1f6571..64919e565 100644 --- a/vscode/src/test/suite/ruby/chruby.test.ts +++ b/vscode/src/test/suite/ruby/chruby.test.ts @@ -9,7 +9,7 @@ import sinon from "sinon"; import { Chruby } from "../../../ruby/chruby"; import { WorkspaceChannel } from "../../../workspaceChannel"; -import { LOG_CHANNEL } from "../../../common"; +import { isWindows, LOG_CHANNEL } from "../../../common"; import { RUBY_VERSION, MAJOR, MINOR, VERSION_REGEX } from "../../rubyVersion"; import { ActivationResult } from "../../../ruby/versionManager"; import { createContext, FakeContext } from "../helpers"; @@ -38,7 +38,7 @@ function createRubySymlinks(destination: string) { } suite("Chruby", () => { - if (os.platform() === "win32") { + if (isWindows()) { // eslint-disable-next-line no-console console.log("Skipping Chruby tests on Windows"); return; diff --git a/vscode/src/test/suite/ruby/custom.test.ts b/vscode/src/test/suite/ruby/custom.test.ts index 0eec0275e..aee241e15 100644 --- a/vscode/src/test/suite/ruby/custom.test.ts +++ b/vscode/src/test/suite/ruby/custom.test.ts @@ -50,7 +50,7 @@ suite("Custom", () => { const activationUri = vscode.Uri.joinPath(context.extensionUri, "activation.rb"); // We must not set the shell on Windows - const shell = os.platform() === "win32" ? undefined : vscode.env.shell; + const shell = common.isWindows() ? undefined : vscode.env.shell; assert.ok( execStub.calledOnceWithExactly( diff --git a/vscode/src/test/suite/ruby/mise.test.ts b/vscode/src/test/suite/ruby/mise.test.ts index beaa1a26d..d3c3e1144 100644 --- a/vscode/src/test/suite/ruby/mise.test.ts +++ b/vscode/src/test/suite/ruby/mise.test.ts @@ -14,7 +14,7 @@ import { ACTIVATION_SEPARATOR, FIELD_SEPARATOR, VALUE_SEPARATOR } from "../../.. import { createContext, FakeContext } from "../helpers"; suite("Mise", () => { - if (os.platform() === "win32") { + if (common.isWindows()) { // eslint-disable-next-line no-console console.log("Skipping Mise tests on Windows"); return; diff --git a/vscode/src/test/suite/ruby/none.test.ts b/vscode/src/test/suite/ruby/none.test.ts index 787993072..38a68749e 100644 --- a/vscode/src/test/suite/ruby/none.test.ts +++ b/vscode/src/test/suite/ruby/none.test.ts @@ -49,7 +49,7 @@ suite("None", () => { const activationUri = vscode.Uri.joinPath(context.extensionUri, "activation.rb"); // We must not set the shell on Windows - const shell = os.platform() === "win32" ? undefined : vscode.env.shell; + const shell = common.isWindows() ? undefined : vscode.env.shell; assert.ok( execStub.calledOnceWithExactly(`ruby -EUTF-8:UTF-8 '${activationUri.fsPath}'`, { diff --git a/vscode/src/test/suite/ruby/rbenv.test.ts b/vscode/src/test/suite/ruby/rbenv.test.ts index 3ed1a7207..e76d5e2cf 100644 --- a/vscode/src/test/suite/ruby/rbenv.test.ts +++ b/vscode/src/test/suite/ruby/rbenv.test.ts @@ -14,7 +14,7 @@ import { ACTIVATION_SEPARATOR, FIELD_SEPARATOR, VALUE_SEPARATOR } from "../../.. import { createContext, FakeContext } from "../helpers"; suite("Rbenv", () => { - if (os.platform() === "win32") { + if (common.isWindows()) { // eslint-disable-next-line no-console console.log("Skipping Rbenv tests on Windows"); return; diff --git a/vscode/src/test/suite/ruby/rubyInstaller.test.ts b/vscode/src/test/suite/ruby/rubyInstaller.test.ts index a962d334f..3fa73f3ea 100644 --- a/vscode/src/test/suite/ruby/rubyInstaller.test.ts +++ b/vscode/src/test/suite/ruby/rubyInstaller.test.ts @@ -16,7 +16,7 @@ import { ACTIVATION_SEPARATOR } from "../../../ruby/versionManager"; import { createRubySymlinks, createContext, FakeContext } from "../helpers"; suite("RubyInstaller", () => { - if (os.platform() !== "win32") { + if (!common.isWindows()) { // eslint-disable-next-line no-console console.log("This test can only run on Windows"); return; diff --git a/vscode/src/test/suite/ruby/rvm.test.ts b/vscode/src/test/suite/ruby/rvm.test.ts index 1d2904f2d..9275c9487 100644 --- a/vscode/src/test/suite/ruby/rvm.test.ts +++ b/vscode/src/test/suite/ruby/rvm.test.ts @@ -13,7 +13,7 @@ import { ACTIVATION_SEPARATOR, FIELD_SEPARATOR, VALUE_SEPARATOR } from "../../.. import { createContext, FakeContext } from "../helpers"; suite("RVM", () => { - if (os.platform() === "win32") { + if (common.isWindows()) { // eslint-disable-next-line no-console console.log("Skipping RVM tests on Windows"); return; diff --git a/vscode/src/test/suite/ruby/shadowenv.test.ts b/vscode/src/test/suite/ruby/shadowenv.test.ts index 8e18b71cb..d36e6ac90 100644 --- a/vscode/src/test/suite/ruby/shadowenv.test.ts +++ b/vscode/src/test/suite/ruby/shadowenv.test.ts @@ -16,7 +16,7 @@ import * as common from "../../../common"; import { createContext, FakeContext } from "../helpers"; suite("Shadowenv", () => { - if (os.platform() === "win32") { + if (common.isWindows()) { // eslint-disable-next-line no-console console.log("Skipping Shadowenv tests on Windows"); return; diff --git a/vscode/src/test/suite/testController.test.ts b/vscode/src/test/suite/testController.test.ts index be904e49a..762914f49 100644 --- a/vscode/src/test/suite/testController.test.ts +++ b/vscode/src/test/suite/testController.test.ts @@ -573,8 +573,9 @@ suite("TestController", () => { test("debugging a test", async () => { await controller.testController.resolveHandler!(undefined); - const manager = - os.platform() === "win32" ? { identifier: ManagerIdentifier.None } : { identifier: ManagerIdentifier.Chruby }; + const manager = common.isWindows() + ? { identifier: ManagerIdentifier.None } + : { identifier: ManagerIdentifier.Chruby }; if (process.env.CI) { createRubySymlinks();