Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ regex = "1.11.1"
serde_json = "1.0"
serde = {version = "1.0", features = ["derive"]}
zed_extension_api = "0.7.0"
anyhow = "1.0.89"

[dev-dependencies]
tree-sitter = "0.25"
Expand Down
54 changes: 26 additions & 28 deletions src/bundler.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::command_executor::CommandExecutor;
use anyhow::{bail, Context, Result};
use std::path::PathBuf;

/// A simple wrapper around the `bundle` command.
Expand Down Expand Up @@ -27,11 +28,7 @@ impl<E: CommandExecutor> Bundler<E> {
///
/// # Returns
/// A `Result` containing the version string if successful, or an error message.
pub fn installed_gem_version(
&self,
name: &str,
envs: &[(&str, &str)],
) -> Result<String, String> {
pub fn installed_gem_version(&self, name: &str, envs: &[(&str, &str)]) -> Result<String> {
let args = &["--version", name];

self.execute_bundle_command("info", args, envs)
Expand All @@ -42,11 +39,11 @@ impl<E: CommandExecutor> Bundler<E> {
cmd: &str,
args: &[&str],
envs: &[(&str, &str)],
) -> Result<String, String> {
) -> Result<String> {
let bundle_gemfile_path = self.working_dir.join("Gemfile");
let bundle_gemfile = bundle_gemfile_path
.to_str()
.ok_or_else(|| "Invalid path to Gemfile".to_string())?;
let bundle_gemfile = bundle_gemfile_path.to_str().with_context(|| {
format!("Invalid path to Gemfile: {}", bundle_gemfile_path.display())
})?;

let full_args: Vec<&str> = std::iter::once(cmd).chain(args.iter().copied()).collect();
let command_envs: Vec<(&str, &str)> = envs
Expand All @@ -55,21 +52,22 @@ impl<E: CommandExecutor> Bundler<E> {
.chain(std::iter::once(("BUNDLE_GEMFILE", bundle_gemfile)))
.collect();

self.command_executor
let output = self
.command_executor
.execute("bundle", &full_args, &command_envs)
.and_then(|output| match output.status {
Some(0) => Ok(String::from_utf8_lossy(&output.stdout).into_owned()),
Some(status) => {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(format!(
"'bundle' command failed (status: {status})\nError: {stderr}",
))
}
None => {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(format!("Failed to execute 'bundle' command: {stderr}"))
}
})
.map_err(|e| anyhow::anyhow!(e))?;

match output.status {
Some(0) => Ok(String::from_utf8_lossy(&output.stdout).into_owned()),
Some(status) => {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("'bundle' command failed (status: {status})\nError: {stderr}")
}
None => {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Failed to execute 'bundle' command: {stderr}")
}
}
}
}

Expand Down Expand Up @@ -214,7 +212,7 @@ mod tests {
result.is_err(),
"Expected error for failed gem version check"
);
let err_msg = result.unwrap_err();
let err_msg = format!("{:#}", result.unwrap_err());
assert!(
err_msg.contains("'bundle' command failed (status: 1)"),
"Error message should contain status"
Expand Down Expand Up @@ -246,10 +244,10 @@ mod tests {
let result = bundler.installed_gem_version(gem_name, &[]);

assert!(result.is_err(), "Expected error from executor failure");
assert_eq!(
result.unwrap_err(),
specific_error_msg,
"Error message should match executor error"
let error_message = format!("{:#}", result.unwrap_err());
assert!(
error_message.contains(specific_error_msg),
"Error message should contain executor error"
);
}
}
80 changes: 43 additions & 37 deletions src/gemset.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::command_executor::CommandExecutor;
use anyhow::{anyhow, bail, Context, Result};
use regex::Regex;
use std::{
collections::hash_map::DefaultHasher,
Expand All @@ -11,10 +12,11 @@ pub fn versioned_gem_home(
base_dir: &Path,
envs: &[(&str, &str)],
executor: &dyn CommandExecutor,
) -> Result<PathBuf, String> {
) -> Result<PathBuf> {
let output = executor
.execute("ruby", &["--version"], envs)
.map_err(|e| format!("Failed to detect Ruby version: {e}"))?;
.map_err(|e| anyhow::anyhow!(e))
.context("Failed to detect Ruby version")?;

match output.status {
Some(0) => {
Expand All @@ -24,8 +26,8 @@ pub fn versioned_gem_home(
let version_hash = format!("{:x}", hasher.finish());
Ok(base_dir.join("gems").join(version_hash))
}
Some(status) => Err(format!("Ruby version check failed with status {status}")),
None => Err("Failed to execute ruby --version".to_string()),
Some(status) => bail!("Ruby version check failed with status {status}"),
None => bail!("Failed to execute ruby --version"),
}
}

Expand Down Expand Up @@ -56,12 +58,12 @@ impl Gemset {
}

/// Returns the full path to a gem binary executable.
pub fn gem_bin_path(&self, bin_name: &str) -> Result<String, String> {
pub fn gem_bin_path(&self, bin_name: &str) -> Result<String> {
let path = self.gem_home.join("bin").join(bin_name);

path.to_str()
.map(ToString::to_string)
.ok_or_else(|| format!("Failed to convert path for '{bin_name}'"))
.with_context(|| format!("Failed to convert path for '{bin_name}'"))
}

pub fn env(&self) -> &[(String, String)] {
Expand Down Expand Up @@ -101,7 +103,7 @@ impl Gemset {
})
}

pub fn install_gem(&self, name: &str) -> Result<(), String> {
pub fn install_gem(&self, name: &str) -> Result<()> {
let args = &[
"--no-user-install",
"--no-format-executable",
Expand All @@ -110,26 +112,26 @@ impl Gemset {
];

self.execute_gem_command("install", args)
.map_err(|e| format!("Failed to install gem '{name}': {e}"))?;
.with_context(|| format!("Failed to install gem '{name}'"))?;

Ok(())
}

pub fn update_gem(&self, name: &str) -> Result<(), String> {
pub fn update_gem(&self, name: &str) -> Result<()> {
self.execute_gem_command("update", &[name])
.map_err(|e| format!("Failed to update gem '{name}': {e}"))?;
.with_context(|| format!("Failed to update gem '{name}'"))?;
Ok(())
}

pub fn uninstall_gem(&self, name: &str, version: &str) -> Result<(), String> {
pub fn uninstall_gem(&self, name: &str, version: &str) -> Result<()> {
let args = &[name, "--version", version];
self.execute_gem_command("uninstall", args)
.map_err(|e| format!("Failed to uninstall gem '{name}': {e}"))?;
.with_context(|| format!("Failed to uninstall gem '{name}' version {version}"))?;

Ok(())
}

pub fn installed_gem_version(&self, name: &str) -> Result<Option<String>, String> {
pub fn installed_gem_version(&self, name: &str) -> Result<Option<String>> {
static GEM_VERSION_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(\S+) \((.+)\)$").unwrap());

Expand All @@ -152,23 +154,23 @@ impl Gemset {
Ok(None)
}

pub fn is_outdated_gem(&self, name: &str) -> Result<bool, String> {
pub fn is_outdated_gem(&self, name: &str) -> Result<bool> {
self.execute_gem_command("outdated", &[]).map(|output| {
output
.lines()
.any(|line| line.split_whitespace().next().is_some_and(|n| n == name))
})
}

fn execute_gem_command(&self, cmd: &str, args: &[&str]) -> Result<String, String> {
fn execute_gem_command(&self, cmd: &str, args: &[&str]) -> Result<String> {
let full_args: Vec<&str> = std::iter::once(cmd)
.chain(std::iter::once("--norc"))
.chain(args.iter().copied())
.collect();
let gem_home_str = self
.gem_home
.to_str()
.ok_or("Failed to convert gem_home path to string")?;
.context("Failed to convert gem_home path to string")?;

let command_envs = vec![("GEM_HOME", gem_home_str)];

Expand All @@ -177,21 +179,22 @@ impl Gemset {
.chain(self.envs.iter().map(|(k, v)| (k.as_str(), v.as_str())))
.collect();

self.command_executor
let output = self
.command_executor
.execute("gem", &full_args, &merged_envs)
.and_then(|output| match output.status {
Some(0) => Ok(String::from_utf8_lossy(&output.stdout).into_owned()),
Some(status) => {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(format!(
"Gem command failed (status: {status})\nError: {stderr}",
))
}
None => {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(format!("Failed to execute gem command: {stderr}"))
}
})
.map_err(|e| anyhow!(e))?;

match output.status {
Some(0) => Ok(String::from_utf8_lossy(&output.stdout).into_owned()),
Some(status) => {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Gem command failed (status: {status})\nError: {stderr}")
}
None => {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Failed to execute gem command: {stderr}")
}
}
}
}

Expand Down Expand Up @@ -396,9 +399,8 @@ mod tests {

let result = versioned_gem_home(Path::new("/extension"), &[], &executor);
assert!(result.is_err());
assert!(result
.expect_err("should return error")
.contains("Ruby version check failed with status 127"));
let error_message = format!("{:#}", result.expect_err("should return error"));
assert!(error_message.contains("Ruby version check failed with status 127"));
}

#[test]
Expand All @@ -413,9 +415,8 @@ mod tests {

let result = versioned_gem_home(Path::new("/extension"), &[], &executor);
assert!(result.is_err());
assert!(result
.expect_err("should return error")
.contains("Failed to detect Ruby version"));
let error_message = format!("{:#}", result.expect_err("should return error"));
assert!(error_message.contains("Failed to detect Ruby version"));
}

#[test]
Expand Down Expand Up @@ -534,6 +535,7 @@ mod tests {
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Failed to install gem 'ruby-lsp'"));
}

Expand Down Expand Up @@ -574,6 +576,7 @@ mod tests {
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Failed to update gem 'ruby-lsp'"));
}

Expand Down Expand Up @@ -667,6 +670,7 @@ mod tests {
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Gem command failed (status: 127)"));
}

Expand Down Expand Up @@ -734,6 +738,7 @@ mod tests {
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Gem command failed (status: 1)"));
}

Expand Down Expand Up @@ -782,6 +787,7 @@ mod tests {
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Failed to uninstall gem 'solargraph'"));
}

Expand All @@ -800,7 +806,7 @@ mod tests {
let gemset = create_gemset(None, mock_executor);
let result = gemset.uninstall_gem(gem_name, gem_version);
assert!(result.is_err());
let error_message = result.unwrap_err();
let error_message = format!("{:#}", result.unwrap_err());
assert!(error_message.contains("Failed to uninstall gem 'solargraph'"));
assert!(error_message.contains("Command not found: gem"));
}
Expand Down
Loading