diff --git a/extension.toml b/extension.toml index 978ade5..b64782d 100644 --- a/extension.toml +++ b/extension.toml @@ -42,6 +42,10 @@ languages = ["HTML+ERB"] name = "Kanayago" languages = ["Ruby"] +[language_servers.fuzzy-ruby-server] +name = "Fuzzy Ruby Server" +languages = ["Ruby"] + [grammars.ruby] repository = "https://github.com/tree-sitter/tree-sitter-ruby" commit = "71bd32fb7607035768799732addba884a37a6210" diff --git a/src/language_servers/fuzzy_ruby_server.rs b/src/language_servers/fuzzy_ruby_server.rs new file mode 100644 index 0000000..1f5a9ac --- /dev/null +++ b/src/language_servers/fuzzy_ruby_server.rs @@ -0,0 +1,150 @@ +use super::language_server::LanguageServerBinary; +use super::LanguageServer; +use zed_extension_api::{self as zed}; + +pub struct FuzzyRubyServer {} + +impl LanguageServer for FuzzyRubyServer { + const SERVER_ID: &str = "fuzzy-ruby-server"; + const EXECUTABLE_NAME: &str = "fuzzy"; + const GEM_NAME: &str = "fuzzy-ruby-server--not-a-gem"; + + fn language_server_binary( + &self, + language_server_id: &zed::LanguageServerId, + worktree: &zed::Worktree, + ) -> zed::Result { + self.resolve_binary(language_server_id.as_ref(), worktree) + } +} + +impl FuzzyRubyServer { + pub fn new() -> Self { + Self {} + } + + fn resolve_binary( + &self, + server_id: &str, + worktree: &T, + ) -> zed::Result { + let lsp_settings = worktree.lsp_binary_settings(server_id)?; + + if let Some(binary_settings) = lsp_settings { + if let Some(path) = binary_settings.path { + if !std::path::Path::new(&path).is_file() { + return Err(format!( + "fuzzy-ruby-server: configured binary path '{}' does not exist or is not a file. Update lsp.fuzzy-ruby-server.binary.path in your Zed settings.", + path + ) + .into()); + } + return Ok(LanguageServerBinary { + path, + args: binary_settings.arguments, + env: Some(worktree.shell_env()), + }); + } + } + + if let Some(path) = worktree.which(Self::EXECUTABLE_NAME) { + return Ok(LanguageServerBinary { + path, + args: Some(self.get_executable_args(worktree)), + env: Some(worktree.shell_env()), + }); + } + + Err("fuzzy not found. Install with: cargo install --git https://github.com/doompling/fuzzy_ruby_server".into()) + } +} + +#[cfg(test)] +mod tests { + use crate::language_servers::{ + language_server::{FakeWorktree, LspBinarySettings}, + FuzzyRubyServer, LanguageServer, + }; + + #[test] + fn test_server_id() { + assert_eq!(FuzzyRubyServer::SERVER_ID, "fuzzy-ruby-server"); + } + + #[test] + fn test_executable_name() { + assert_eq!(FuzzyRubyServer::EXECUTABLE_NAME, "fuzzy"); + } + + #[test] + fn test_executable_args() { + let server = FuzzyRubyServer::new(); + let mock_worktree = FakeWorktree::new("/path/to/project".to_string()); + assert_eq!(server.get_executable_args(&mock_worktree), Vec::::new()); + } + + #[test] + fn test_language_server_binary_custom_path() { + let server = FuzzyRubyServer::new(); + let mut mock_worktree = FakeWorktree::new("/path/to/project".to_string()); + // Use a path that actually exists on the system for the custom-path test + let real_path = std::env::current_exe() + .expect("could not get test binary path") + .to_string_lossy() + .to_string(); + mock_worktree.add_lsp_binary_setting( + FuzzyRubyServer::SERVER_ID.to_string(), + Ok(Some(LspBinarySettings { + path: Some(real_path.clone()), + arguments: None, + })), + ); + let result = server.resolve_binary(FuzzyRubyServer::SERVER_ID, &mock_worktree); + assert!(result.is_ok()); + assert_eq!(result.unwrap().path, real_path); + } + + #[test] + fn test_language_server_binary_custom_path_missing() { + let server = FuzzyRubyServer::new(); + let mut mock_worktree = FakeWorktree::new("/path/to/project".to_string()); + mock_worktree.add_lsp_binary_setting( + FuzzyRubyServer::SERVER_ID.to_string(), + Ok(Some(LspBinarySettings { + path: Some("/nonexistent/fuzzy".to_string()), + arguments: None, + })), + ); + let result = server.resolve_binary(FuzzyRubyServer::SERVER_ID, &mock_worktree); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + err.contains("does not exist or is not a file"), + "Error was: {err}" + ); + } + + #[test] + fn test_language_server_binary_path_lookup() { + let server = FuzzyRubyServer::new(); + let mut mock_worktree = FakeWorktree::new("/path/to/project".to_string()); + mock_worktree.add_lsp_binary_setting(FuzzyRubyServer::SERVER_ID.to_string(), Ok(None)); + mock_worktree.set_which("fuzzy".to_string(), Some("/usr/local/bin/fuzzy".to_string())); + let result = server.resolve_binary(FuzzyRubyServer::SERVER_ID, &mock_worktree); + assert!(result.is_ok()); + assert_eq!(result.unwrap().path, "/usr/local/bin/fuzzy"); + } + + #[test] + fn test_language_server_binary_not_found() { + let server = FuzzyRubyServer::new(); + let mut mock_worktree = FakeWorktree::new("/path/to/project".to_string()); + mock_worktree.add_lsp_binary_setting(FuzzyRubyServer::SERVER_ID.to_string(), Ok(None)); + mock_worktree.set_which("fuzzy".to_string(), None); + let result = server.resolve_binary(FuzzyRubyServer::SERVER_ID, &mock_worktree); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.contains("fuzzy not found"), "Error was: {err}"); + assert!(err.contains("cargo install"), "Error was: {err}"); + } +} diff --git a/src/language_servers/language_server.rs b/src/language_servers/language_server.rs index 816d8e6..8542ebc 100644 --- a/src/language_servers/language_server.rs +++ b/src/language_servers/language_server.rs @@ -30,6 +30,7 @@ pub trait WorktreeLike { fn shell_env(&self) -> Vec<(String, String)>; fn read_text_file(&self, path: &str) -> Result; fn lsp_binary_settings(&self, server_id: &str) -> Result, String>; + fn which(&self, name: &str) -> Option; } impl WorktreeLike for zed::Worktree { @@ -54,6 +55,10 @@ impl WorktreeLike for zed::Worktree { Err(e) => Err(e), } } + + fn which(&self, name: &str) -> Option { + zed::Worktree::which(self, name) + } } #[cfg(test)] @@ -62,6 +67,7 @@ pub struct FakeWorktree { shell_env: Vec<(String, String)>, files: HashMap>, lsp_binary_settings_map: HashMap, String>>, + which_map: HashMap>, } #[cfg(test)] @@ -72,6 +78,7 @@ impl FakeWorktree { shell_env: Vec::new(), files: HashMap::new(), lsp_binary_settings_map: HashMap::new(), + which_map: HashMap::new(), } } @@ -86,6 +93,10 @@ impl FakeWorktree { ) { self.lsp_binary_settings_map.insert(server_id, settings); } + + pub fn set_which(&mut self, name: String, result: Option) { + self.which_map.insert(name, result); + } } #[cfg(test)] @@ -111,6 +122,10 @@ impl WorktreeLike for FakeWorktree { .cloned() .unwrap_or(Ok(None)) } + + fn which(&self, name: &str) -> Option { + self.which_map.get(name).cloned().flatten() + } } pub trait LanguageServer { diff --git a/src/language_servers/mod.rs b/src/language_servers/mod.rs index 8360f25..3641508 100644 --- a/src/language_servers/mod.rs +++ b/src/language_servers/mod.rs @@ -1,3 +1,4 @@ +mod fuzzy_ruby_server; mod herb; mod kanayago; mod language_server; @@ -7,6 +8,7 @@ mod solargraph; mod sorbet; mod steep; +pub use fuzzy_ruby_server::FuzzyRubyServer; pub use herb::Herb; pub use kanayago::Kanayago; pub use language_server::LanguageServer; diff --git a/src/ruby.rs b/src/ruby.rs index 2ee387a..5a775a1 100644 --- a/src/ruby.rs +++ b/src/ruby.rs @@ -9,7 +9,7 @@ use bundler::Bundler; use command_executor::RealCommandExecutor; use gemset::{versioned_gem_home, Gemset}; use language_servers::{ - Herb, Kanayago, LanguageServer, Rubocop, RubyLsp, Solargraph, Sorbet, Steep, + FuzzyRubyServer, Herb, Kanayago, LanguageServer, Rubocop, RubyLsp, Solargraph, Sorbet, Steep, }; use serde::{Deserialize, Serialize}; use zed_extension_api::{ @@ -27,6 +27,7 @@ struct RubyExtension { steep: Option, herb: Option, kanayago: Option, + fuzzy_ruby_server: Option, } #[derive(Serialize, Deserialize)] @@ -80,6 +81,10 @@ impl zed::Extension for RubyExtension { let kanayago = self.kanayago.get_or_insert_with(Kanayago::new); kanayago.language_server_command(language_server_id, worktree) } + FuzzyRubyServer::SERVER_ID => { + let server = self.fuzzy_ruby_server.get_or_insert_with(FuzzyRubyServer::new); + server.language_server_command(language_server_id, worktree) + } language_server_id => Err(format!("unknown language server: {language_server_id}")), } }