diff --git a/src/bundler.rs b/src/bundler.rs index 88116f3..0d8d69a 100644 --- a/src/bundler.rs +++ b/src/bundler.rs @@ -1,34 +1,6 @@ +use crate::command_executor::CommandExecutor; use std::path::Path; -use zed_extension_api::{process::Output, Command, Result}; - -pub trait CommandExecutor { - fn execute_bundle( - &self, - sub_command: String, - args: Vec, - envs: Vec<(String, String)>, - bundle_gemfile_path: &str, - ) -> Result; -} - -pub struct RealCommandExecutor; - -impl CommandExecutor for RealCommandExecutor { - fn execute_bundle( - &self, - sub_command: String, - args: Vec, - envs: Vec<(String, String)>, - bundle_gemfile_path: &str, - ) -> Result { - Command::new("bundle") - .arg(sub_command) - .args(args) - .envs(envs) - .env("BUNDLE_GEMFILE", bundle_gemfile_path) - .output() - } -} +use zed_extension_api::Result; /// A simple wrapper around the `bundle` command. pub struct Bundler { @@ -74,8 +46,19 @@ impl Bundler { .to_str() .ok_or_else(|| "Invalid path to Gemfile".to_string())?; + let full_args: Vec = std::iter::once(cmd).chain(args).collect(); + let command_envs: Vec<(String, String)> = self + .envs + .iter() + .cloned() + .chain(std::iter::once(( + "BUNDLE_GEMFILE".to_string(), + bundle_gemfile.to_string(), + ))) + .collect(); + self.command_executor - .execute_bundle(cmd, args, self.envs.clone(), bundle_gemfile) + .execute("bundle", full_args, command_envs) .and_then(|output| match output.status { Some(0) => Ok(String::from_utf8_lossy(&output.stdout).to_string()), Some(status) => { @@ -96,14 +79,15 @@ impl Bundler { #[cfg(test)] mod tests { use super::*; + use crate::command_executor::CommandExecutor; use std::cell::RefCell; + use zed_extension_api::process::Output; struct MockExecutorConfig { output_to_return: Option>, - expected_sub_command: Option, + expected_command_name: Option, expected_args: Option>, expected_envs: Option>, - expected_bundle_gemfile_path: Option, } struct MockCommandExecutor { @@ -115,47 +99,44 @@ mod tests { MockCommandExecutor { config: RefCell::new(MockExecutorConfig { output_to_return: None, - expected_sub_command: None, + expected_command_name: None, expected_args: None, expected_envs: None, - expected_bundle_gemfile_path: None, }), } } fn expect( &self, - sub_command: &str, - args: &[&str], - envs: &[(&str, &str)], - bundle_gemfile_path: &str, - output: super::Result, + command_name: &str, + full_args: &[&str], + final_envs: &[(&str, &str)], + output: Result, ) { let mut config = self.config.borrow_mut(); - config.expected_sub_command = Some(sub_command.to_string()); - config.expected_args = Some(args.iter().map(|s| s.to_string()).collect()); + config.expected_command_name = Some(command_name.to_string()); + config.expected_args = Some(full_args.iter().map(|s| s.to_string()).collect()); config.expected_envs = Some( - envs.iter() + final_envs + .iter() .map(|&(k, v)| (k.to_string(), v.to_string())) .collect(), ); - config.expected_bundle_gemfile_path = Some(bundle_gemfile_path.to_string()); config.output_to_return = Some(output); } } impl CommandExecutor for MockCommandExecutor { - fn execute_bundle( + fn execute( &self, - sub_command: String, + command_name: &str, args: Vec, envs: Vec<(String, String)>, - bundle_gemfile_path: &str, - ) -> super::Result { + ) -> Result { let mut config = self.config.borrow_mut(); - if let Some(expected_cmd) = &config.expected_sub_command { - assert_eq!(&sub_command, expected_cmd, "Mock: Sub-command mismatch"); + if let Some(expected_name) = &config.expected_command_name { + assert_eq!(command_name, expected_name, "Mock: Command name mismatch"); } if let Some(expected_args) = &config.expected_args { assert_eq!(&args, expected_args, "Mock: Args mismatch"); @@ -163,16 +144,10 @@ mod tests { if let Some(expected_envs) = &config.expected_envs { assert_eq!(&envs, expected_envs, "Mock: Env mismatch"); } - if let Some(expected_path) = &config.expected_bundle_gemfile_path { - assert_eq!( - bundle_gemfile_path, expected_path, - "Mock: Gemfile path mismatch" - ); - } config.output_to_return.take().expect( - "MockCommandExecutor: output_to_return was not set or already consumed for the test", - ) + "MockCommandExecutor: output_to_return was not set or already consumed for the test", + ) } } @@ -182,11 +157,11 @@ mod tests { gem: &str, ) -> MockCommandExecutor { let mock = MockCommandExecutor::new(); + let gemfile_path = format!("{}/Gemfile", dir); mock.expect( - "info", - &["--version", gem], - &[], - &format!("{}/Gemfile", dir), + "bundle", + &["info", "--version", gem], + &[("BUNDLE_GEMFILE", &gemfile_path)], Ok(Output { status: Some(0), stdout: version.as_bytes().to_vec(), @@ -211,12 +186,12 @@ mod tests { let mock_executor = MockCommandExecutor::new(); let gem_name = "unknown_gem"; let error_output = "Could not find gem 'unknown_gem'."; + let gemfile_path = "test_dir/Gemfile"; mock_executor.expect( - "info", - &["--version", gem_name], - &[], - "test_dir/Gemfile", + "bundle", + &["info", "--version", gem_name], + &[("BUNDLE_GEMFILE", gemfile_path)], Ok(Output { status: Some(1), stdout: Vec::new(), @@ -247,12 +222,12 @@ mod tests { let mock_executor = MockCommandExecutor::new(); let gem_name = "critical_gem"; let specific_error_msg = "Mocked execution failure"; + let gemfile_path = "test_dir/Gemfile"; mock_executor.expect( - "info", - &["--version", gem_name], - &[], - "test_dir/Gemfile", + "bundle", + &["info", "--version", gem_name], + &[("BUNDLE_GEMFILE", gemfile_path)], Err(specific_error_msg.to_string()), ); diff --git a/src/command_executor.rs b/src/command_executor.rs new file mode 100644 index 0000000..030dd01 --- /dev/null +++ b/src/command_executor.rs @@ -0,0 +1,30 @@ +use zed_extension_api::{process::Output, Command as ZedCommand, Result}; + +pub trait CommandExecutor { + /// Executes a command with the given arguments and environment variables. + /// + /// # Arguments + /// + /// * `cmd` - The name or path of the command to execute (e.g., "gem", "bundle"). + /// * `args` - A vector of string arguments to pass to the command. + /// * `envs` - A vector of key-value pairs representing environment variables + /// to set for the command's execution context. + /// + /// # Returns + /// + /// A `Result` containing the `Output` of the command if successful. The `Output` + /// typically includes stdout, stderr, and the exit status. Returns an error + /// if the command execution fails at a lower level (e.g., command not found, + /// or if the `zed_extension_api::Command` itself returns an error). + fn execute(&self, cmd: &str, args: Vec, envs: Vec<(String, String)>) -> Result; +} + +/// An implementation of `CommandExecutor` that executes commands +/// using the `zed_extension_api::Command`. +pub struct RealCommandExecutor; + +impl CommandExecutor for RealCommandExecutor { + fn execute(&self, cmd: &str, args: Vec, envs: Vec<(String, String)>) -> Result { + ZedCommand::new(cmd).args(args).envs(envs).output() + } +} diff --git a/src/gemset.rs b/src/gemset.rs index e49ee2f..c0251bf 100644 --- a/src/gemset.rs +++ b/src/gemset.rs @@ -1,37 +1,16 @@ use regex::Regex; -use zed_extension_api::{process::Output, Command, Result}; +use zed_extension_api::Result; -pub trait GemCommandExecutor { - fn execute_gem(&self, gem_home: &str, sub_command: String, args: Vec) - -> Result; -} - -pub struct RealGemCommandExecutor; - -impl GemCommandExecutor for RealGemCommandExecutor { - fn execute_gem( - &self, - gem_home: &str, - sub_command: String, - args: Vec, - ) -> Result { - Command::new("gem") - .env("GEM_HOME", gem_home) - .arg(sub_command) - .arg("--norc") - .args(args) - .output() - } -} +use crate::command_executor::CommandExecutor; /// A simple wrapper around the `gem` command. pub struct Gemset { pub gem_home: String, - command_executor: Box, + command_executor: Box, } impl Gemset { - pub fn new(gem_home: String, command_executor: Box) -> Self { + pub fn new(gem_home: String, command_executor: Box) -> Self { Self { gem_home, command_executor, @@ -109,9 +88,15 @@ impl Gemset { }) } - fn execute_gem_command(&self, command: String, args: Vec) -> Result { + fn execute_gem_command(&self, cmd: String, args: Vec) -> Result { + let full_args: Vec = std::iter::once(cmd) + .chain(std::iter::once("--norc".to_string())) + .chain(args) + .collect(); + let command_envs = vec![("GEM_HOME".to_string(), self.gem_home.clone())]; + self.command_executor - .execute_gem(&self.gem_home, command, args) + .execute("gem", full_args, command_envs) .and_then(|output| match output.status { Some(0) => Ok(String::from_utf8_lossy(&output.stdout).to_string()), Some(status) => { @@ -132,12 +117,14 @@ impl Gemset { #[cfg(test)] mod tests { use super::*; + use crate::command_executor::CommandExecutor; use std::cell::RefCell; + use zed_extension_api::process::Output; struct MockExecutorConfig { - expected_gem_home: Option, - expected_sub_command: Option, + expected_command_name: Option, expected_args: Option>, + expected_envs: Option>, output_to_return: Option>, } @@ -149,41 +136,52 @@ mod tests { fn new() -> Self { MockGemCommandExecutor { config: RefCell::new(MockExecutorConfig { - expected_gem_home: None, - expected_sub_command: None, + expected_command_name: None, expected_args: None, + expected_envs: None, output_to_return: None, }), } } - fn expect(&self, gem_home: &str, sub_command: &str, args: &[&str], output: Result) { + fn expect( + &self, + command_name: &str, + full_args: &[&str], + final_envs: &[(&str, &str)], + output: Result, + ) { let mut config = self.config.borrow_mut(); - config.expected_gem_home = Some(gem_home.to_string()); - config.expected_sub_command = Some(sub_command.to_string()); - config.expected_args = Some(args.iter().map(|s| s.to_string()).collect()); + config.expected_command_name = Some(command_name.to_string()); + config.expected_args = Some(full_args.iter().map(|s| s.to_string()).collect()); + config.expected_envs = Some( + final_envs + .iter() + .map(|&(k, v)| (k.to_string(), v.to_string())) + .collect(), + ); config.output_to_return = Some(output); } } - impl GemCommandExecutor for MockGemCommandExecutor { - fn execute_gem( + impl CommandExecutor for MockGemCommandExecutor { + fn execute( &self, - gem_home: &str, - sub_command: String, + command_name: &str, args: Vec, + envs: Vec<(String, String)>, ) -> Result { let mut config = self.config.borrow_mut(); - if let Some(expected_home) = &config.expected_gem_home { - assert_eq!(gem_home, expected_home, "Mock: GEM_HOME mismatch"); - } - if let Some(expected_cmd) = &config.expected_sub_command { - assert_eq!(&sub_command, expected_cmd, "Mock: Sub-command mismatch"); + if let Some(expected_name) = &config.expected_command_name { + assert_eq!(command_name, expected_name, "Mock: Command name mismatch"); } if let Some(expected_args) = &config.expected_args { assert_eq!(&args, expected_args, "Mock: Args mismatch"); } + if let Some(expected_envs) = &config.expected_envs { + assert_eq!(&envs, expected_envs, "Mock: Env mismatch"); + } config .output_to_return @@ -225,14 +223,16 @@ mod tests { let mock_executor = MockGemCommandExecutor::new(); let gem_name = "ruby-lsp"; mock_executor.expect( - TEST_GEM_HOME, - "install", + "gem", &[ + "install", + "--norc", "--no-user-install", "--no-format-executable", "--no-document", gem_name, ], + &[("GEM_HOME", TEST_GEM_HOME)], Ok(Output { status: Some(0), stdout: "Successfully installed ruby-lsp-1.0.0".as_bytes().to_vec(), @@ -248,14 +248,16 @@ mod tests { let mock_executor = MockGemCommandExecutor::new(); let gem_name = "ruby-lsp"; mock_executor.expect( - TEST_GEM_HOME, - "install", + "gem", &[ + "install", + "--norc", "--no-user-install", "--no-format-executable", "--no-document", gem_name, ], + &[("GEM_HOME", TEST_GEM_HOME)], Ok(Output { status: Some(1), stdout: Vec::new(), @@ -275,9 +277,9 @@ mod tests { let mock_executor = MockGemCommandExecutor::new(); let gem_name = "ruby-lsp"; mock_executor.expect( - TEST_GEM_HOME, - "update", - &[gem_name], + "gem", + &["update", "--norc", gem_name], + &[("GEM_HOME", TEST_GEM_HOME)], Ok(Output { status: Some(0), stdout: "Gems updated: ruby-lsp".as_bytes().to_vec(), @@ -293,9 +295,9 @@ mod tests { let mock_executor = MockGemCommandExecutor::new(); let gem_name = "ruby-lsp"; mock_executor.expect( - TEST_GEM_HOME, - "update", - &[gem_name], + "gem", + &["update", "--norc", gem_name], + &[("GEM_HOME", TEST_GEM_HOME)], Ok(Output { status: Some(1), stdout: Vec::new(), @@ -321,9 +323,9 @@ mod tests { ); mock_executor.expect( - TEST_GEM_HOME, - "list", - &["--exact", gem_name], + "gem", + &["list", "--norc", "--exact", gem_name], + &[("GEM_HOME", TEST_GEM_HOME)], Ok(Output { status: Some(0), stdout: gem_list_output.as_bytes().to_vec(), @@ -346,9 +348,9 @@ mod tests { ); mock_executor.expect( - TEST_GEM_HOME, - "list", - &["--exact", gem_name], + "gem", + &["list", "--norc", "--exact", gem_name], + &[("GEM_HOME", TEST_GEM_HOME)], Ok(Output { status: Some(0), stdout: gem_list_output.as_bytes().to_vec(), @@ -367,9 +369,9 @@ mod tests { let gem_list_output = "other_gem (1.0.0)\nanother_gem (2.0.0)"; mock_executor.expect( - TEST_GEM_HOME, - "list", - &["--exact", gem_name], + "gem", + &["list", "--norc", "--exact", gem_name], + &[("GEM_HOME", TEST_GEM_HOME)], Ok(Output { status: Some(0), stdout: gem_list_output.as_bytes().to_vec(), @@ -386,9 +388,9 @@ mod tests { let mock_executor = MockGemCommandExecutor::new(); let gem_name = "ruby-lsp"; mock_executor.expect( - TEST_GEM_HOME, - "list", - &["--exact", gem_name], + "gem", + &["list", "--norc", "--exact", gem_name], + &[("GEM_HOME", TEST_GEM_HOME)], Ok(Output { status: Some(127), stdout: Vec::new(), @@ -413,9 +415,9 @@ mod tests { ); mock_executor.expect( - TEST_GEM_HOME, - "outdated", - &[], + "gem", + &["outdated", "--norc"], + &[("GEM_HOME", TEST_GEM_HOME)], Ok(Output { status: Some(0), stdout: outdated_output.as_bytes().to_vec(), @@ -434,9 +436,9 @@ mod tests { let outdated_output = "csv (3.3.2 < 3.3.4)"; mock_executor.expect( - TEST_GEM_HOME, - "outdated", - &[], + "gem", + &["outdated", "--norc"], + &[("GEM_HOME", TEST_GEM_HOME)], Ok(Output { status: Some(0), stdout: outdated_output.as_bytes().to_vec(), @@ -453,9 +455,9 @@ mod tests { let mock_executor = MockGemCommandExecutor::new(); let gem_name = "ruby-lsp"; mock_executor.expect( - TEST_GEM_HOME, - "outdated", - &[], + "gem", + &["outdated", "--norc"], + &[("GEM_HOME", TEST_GEM_HOME)], Ok(Output { status: Some(1), stdout: Vec::new(), diff --git a/src/language_servers/language_server.rs b/src/language_servers/language_server.rs index 57e426f..5c48a2e 100644 --- a/src/language_servers/language_server.rs +++ b/src/language_servers/language_server.rs @@ -3,10 +3,7 @@ use zed_extension_api::{ LanguageServerInstallationStatus, Result, }; -use crate::{ - bundler::{Bundler, RealCommandExecutor}, - gemset::{Gemset, RealGemCommandExecutor}, -}; +use crate::{bundler::Bundler, command_executor::RealCommandExecutor, gemset::Gemset}; #[derive(Clone, Debug)] pub struct LanguageServerBinary { @@ -121,7 +118,7 @@ pub trait LanguageServer { .to_string_lossy() .to_string(); - let gemset = Gemset::new(gem_home.clone(), Box::new(RealGemCommandExecutor)); + let gemset = Gemset::new(gem_home.clone(), Box::new(RealCommandExecutor)); set_language_server_installation_status( language_server_id, diff --git a/src/language_servers.rs b/src/language_servers/mod.rs similarity index 100% rename from src/language_servers.rs rename to src/language_servers/mod.rs diff --git a/src/ruby.rs b/src/ruby.rs index 752c287..040b8fc 100644 --- a/src/ruby.rs +++ b/src/ruby.rs @@ -1,4 +1,5 @@ mod bundler; +mod command_executor; mod gemset; mod language_servers; use language_servers::{LanguageServer, Rubocop, RubyLsp, Solargraph};