diff --git a/Cargo.lock b/Cargo.lock index 609c73d..46cf55b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "auditable-serde" @@ -1110,6 +1110,7 @@ dependencies = [ name = "zed_ruby" version = "0.16.5" dependencies = [ + "anyhow", "insta", "regex", "serde", diff --git a/Cargo.toml b/Cargo.toml index 7424459..0f57e10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/bundler.rs b/src/bundler.rs index dfd8d7f..4e7e214 100644 --- a/src/bundler.rs +++ b/src/bundler.rs @@ -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. @@ -27,11 +28,7 @@ impl Bundler { /// /// # 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 { + pub fn installed_gem_version(&self, name: &str, envs: &[(&str, &str)]) -> Result { let args = &["--version", name]; self.execute_bundle_command("info", args, envs) @@ -42,11 +39,11 @@ impl Bundler { cmd: &str, args: &[&str], envs: &[(&str, &str)], - ) -> Result { + ) -> Result { 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 @@ -55,21 +52,22 @@ impl Bundler { .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}") + } + } } } @@ -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" @@ -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" ); } } diff --git a/src/gemset.rs b/src/gemset.rs index 88ca293..e3bd387 100644 --- a/src/gemset.rs +++ b/src/gemset.rs @@ -1,4 +1,5 @@ use crate::command_executor::CommandExecutor; +use anyhow::{anyhow, bail, Context, Result}; use regex::Regex; use std::{ collections::hash_map::DefaultHasher, @@ -11,10 +12,11 @@ pub fn versioned_gem_home( base_dir: &Path, envs: &[(&str, &str)], executor: &dyn CommandExecutor, -) -> Result { +) -> Result { 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) => { @@ -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"), } } @@ -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 { + pub fn gem_bin_path(&self, bin_name: &str) -> Result { 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)] { @@ -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", @@ -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, String> { + pub fn installed_gem_version(&self, name: &str) -> Result> { static GEM_VERSION_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^(\S+) \((.+)\)$").unwrap()); @@ -152,7 +154,7 @@ impl Gemset { Ok(None) } - pub fn is_outdated_gem(&self, name: &str) -> Result { + pub fn is_outdated_gem(&self, name: &str) -> Result { self.execute_gem_command("outdated", &[]).map(|output| { output .lines() @@ -160,7 +162,7 @@ impl Gemset { }) } - fn execute_gem_command(&self, cmd: &str, args: &[&str]) -> Result { + fn execute_gem_command(&self, cmd: &str, args: &[&str]) -> Result { let full_args: Vec<&str> = std::iter::once(cmd) .chain(std::iter::once("--norc")) .chain(args.iter().copied()) @@ -168,7 +170,7 @@ impl Gemset { 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)]; @@ -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}") + } + } } } @@ -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] @@ -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] @@ -534,6 +535,7 @@ mod tests { assert!(result.is_err()); assert!(result .unwrap_err() + .to_string() .contains("Failed to install gem 'ruby-lsp'")); } @@ -574,6 +576,7 @@ mod tests { assert!(result.is_err()); assert!(result .unwrap_err() + .to_string() .contains("Failed to update gem 'ruby-lsp'")); } @@ -667,6 +670,7 @@ mod tests { assert!(result.is_err()); assert!(result .unwrap_err() + .to_string() .contains("Gem command failed (status: 127)")); } @@ -734,6 +738,7 @@ mod tests { assert!(result.is_err()); assert!(result .unwrap_err() + .to_string() .contains("Gem command failed (status: 1)")); } @@ -782,6 +787,7 @@ mod tests { assert!(result.is_err()); assert!(result .unwrap_err() + .to_string() .contains("Failed to uninstall gem 'solargraph'")); } @@ -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")); } diff --git a/src/language_servers/language_server.rs b/src/language_servers/language_server.rs index d529737..816d8e6 100644 --- a/src/language_servers/language_server.rs +++ b/src/language_servers/language_server.rs @@ -219,7 +219,7 @@ pub trait LanguageServer { worktree: &zed::Worktree, ) -> zed::Result { let base_dir = std::env::current_dir() - .map_err(|e| format!("Failed to get extension directory: {e}"))?; + .map_err(|e| format!("Failed to get extension directory: {e:#}"))?; let worktree_shell_env = worktree.shell_env(); let worktree_shell_env_vars: Vec<(&str, &str)> = worktree_shell_env @@ -228,7 +228,8 @@ pub trait LanguageServer { .collect(); let gem_home = - versioned_gem_home(&base_dir, &worktree_shell_env_vars, &RealCommandExecutor)?; + versioned_gem_home(&base_dir, &worktree_shell_env_vars, &RealCommandExecutor) + .map_err(|e| format!("{:#}", e))?; let gemset = Gemset::new( gem_home, @@ -244,7 +245,7 @@ pub trait LanguageServer { Ok(Some(version)) => { if gemset .is_outdated_gem(Self::GEM_NAME) - .map_err(|e| e.to_string())? + .map_err(|e| format!("{:#}", e))? { zed::set_language_server_installation_status( language_server_id, @@ -253,16 +254,23 @@ pub trait LanguageServer { gemset .update_gem(Self::GEM_NAME) - .map_err(|e| e.to_string())?; - - gemset - .uninstall_gem(Self::GEM_NAME, &version) - .map_err(|e| e.to_string())?; + .map_err(|e| format!("{:#}", e))?; + + // Try to uninstall old version, but don't fail if it errors + // The new version is already installed and working + if let Err(e) = gemset.uninstall_gem(Self::GEM_NAME, &version) { + eprintln!( + "Warning: Failed to uninstall old version {} of {}: {:#}", + version, + Self::GEM_NAME, + e + ); + } } let executable_path = gemset .gem_bin_path(Self::EXECUTABLE_NAME) - .map_err(|e| e.to_string())?; + .map_err(|e| format!("{:#}", e))?; Ok(LanguageServerBinary { path: executable_path, @@ -278,11 +286,11 @@ pub trait LanguageServer { gemset .install_gem(Self::GEM_NAME) - .map_err(|e| e.to_string())?; + .map_err(|e| format!("{:#}", e))?; let executable_path = gemset .gem_bin_path(Self::EXECUTABLE_NAME) - .map_err(|e| e.to_string())?; + .map_err(|e| format!("{:#}", e))?; Ok(LanguageServerBinary { path: executable_path, @@ -290,7 +298,7 @@ pub trait LanguageServer { env: Some(gemset.env().to_vec()), }) } - Err(e) => Err(e), + Err(e) => Err(format!("{:#}", e)), } } } diff --git a/src/ruby.rs b/src/ruby.rs index 9a77886..9b05214 100644 --- a/src/ruby.rs +++ b/src/ruby.rs @@ -144,13 +144,16 @@ impl zed::Extension for RubyExtension { (path, Vec::new()) } else { let base_dir = std::env::current_dir() - .map_err(|e| format!("Failed to get extension directory: {e}"))?; - let gem_home = versioned_gem_home(&base_dir, &env_vars, &RealCommandExecutor)?; + .map_err(|e| format!("Failed to get extension directory: {e:#}"))?; + let gem_home = versioned_gem_home(&base_dir, &env_vars, &RealCommandExecutor) + .map_err(|e| format!("{:#}", e))?; let gemset = Gemset::new(gem_home, Some(&env_vars), Box::new(RealCommandExecutor)); gemset .install_gem("debug") - .map_err(|e| format!("Failed to install debug gem: {e}"))?; - let rdbg = gemset.gem_bin_path("rdbg")?; + .map_err(|e| format!("Failed to install debug gem: {e:#}"))?; + let rdbg = gemset + .gem_bin_path("rdbg") + .map_err(|e| format!("{:#}", e))?; (rdbg, Vec::new()) } }; @@ -162,7 +165,7 @@ impl zed::Extension for RubyExtension { }); let mut connection = resolve_tcp_template(tcp_connection)?; let mut configuration: serde_json::Value = serde_json::from_str(&config.config) - .map_err(|e| format!("`config` is not a valid JSON: {e}"))?; + .map_err(|e| format!("`config` is not a valid JSON: {e:#}"))?; if let Some(configuration) = configuration.as_object_mut() { configuration .entry("cwd") @@ -170,7 +173,7 @@ impl zed::Extension for RubyExtension { } let ruby_config: RubyDebugConfig = serde_json::from_value(configuration.clone()) - .map_err(|e| format!("`config` is not a valid rdbg config: {e}"))?; + .map_err(|e| format!("`config` is not a valid rdbg config: {e:#}"))?; if let Some(host) = ruby_config.env.get("RUBY_DEBUG_HOST") { connection.host = host