diff --git a/extension.toml b/extension.toml index 4d84156..c2cd9f1 100644 --- a/extension.toml +++ b/extension.toml @@ -22,6 +22,10 @@ languages = ["Ruby"] name = "Steep" languages = ["Ruby"] +[language_servers.sorbet] +name = "Sorbet" +languages = ["Ruby"] + [grammars.ruby] repository = "https://github.com/tree-sitter/tree-sitter-ruby" commit = "71bd32fb7607035768799732addba884a37a6210" diff --git a/src/language_servers/language_server.rs b/src/language_servers/language_server.rs index dda2bc9..05cbac1 100644 --- a/src/language_servers/language_server.rs +++ b/src/language_servers/language_server.rs @@ -1,3 +1,6 @@ +#[cfg(test)] +use std::collections::HashMap; + use crate::{bundler::Bundler, command_executor::RealCommandExecutor, gemset::Gemset}; use zed_extension_api::{self as zed}; @@ -8,28 +11,43 @@ pub struct LanguageServerBinary { pub env: Option>, } +#[derive(Clone, Debug, Default)] +pub struct LspBinarySettings { + #[allow(dead_code)] + pub path: Option, + pub arguments: Option>, +} + pub trait WorktreeLike { #[allow(dead_code)] fn root_path(&self) -> String; - #[allow(dead_code)] fn shell_env(&self) -> Vec<(String, String)>; - - #[allow(dead_code)] fn read_text_file(&self, path: &str) -> Result; + fn lsp_binary_settings(&self, server_id: &str) -> Result, String>; } impl WorktreeLike for zed::Worktree { fn root_path(&self) -> String { - self.root_path() + zed::Worktree::root_path(self) } fn shell_env(&self) -> Vec<(String, String)> { - self.shell_env() + zed::Worktree::shell_env(self) } fn read_text_file(&self, path: &str) -> Result { - self.read_text_file(path) + zed::Worktree::read_text_file(self, path) + } + + fn lsp_binary_settings(&self, server_id: &str) -> Result, String> { + match zed::settings::LspSettings::for_worktree(server_id, self) { + Ok(lsp_settings) => Ok(lsp_settings.binary.map(|b| LspBinarySettings { + path: b.path, + arguments: b.arguments, + })), + Err(e) => Err(e), + } } } @@ -37,6 +55,8 @@ impl WorktreeLike for zed::Worktree { pub struct FakeWorktree { root_path: String, shell_env: Vec<(String, String)>, + files: HashMap>, + lsp_binary_settings_map: HashMap, String>>, } #[cfg(test)] @@ -45,11 +65,21 @@ impl FakeWorktree { FakeWorktree { root_path, shell_env: Vec::new(), + files: HashMap::new(), + lsp_binary_settings_map: HashMap::new(), } } - fn read_text_file(&self, _path: &str) -> Result { - Ok(String::new()) + pub fn add_file(&mut self, path: String, content: Result) { + self.files.insert(path, content); + } + + pub fn add_lsp_binary_setting( + &mut self, + server_id: String, + settings: Result, String>, + ) { + self.lsp_binary_settings_map.insert(server_id, settings); } } @@ -64,7 +94,17 @@ impl WorktreeLike for FakeWorktree { } fn read_text_file(&self, path: &str) -> Result { - self.read_text_file(path) + self.files + .get(path) + .cloned() + .unwrap_or_else(|| Err(format!("File not found in mock: {}", path))) + } + + fn lsp_binary_settings(&self, server_id: &str) -> Result, String> { + self.lsp_binary_settings_map + .get(server_id) + .cloned() + .unwrap_or(Ok(None)) } } @@ -104,11 +144,11 @@ pub trait LanguageServer { let lsp_settings = zed::settings::LspSettings::for_worktree(language_server_id.as_ref(), worktree)?; - if let Some(binary_settings) = lsp_settings.binary { - if let Some(path) = binary_settings.path { + if let Some(binary_settings) = &lsp_settings.binary { + if let Some(path) = &binary_settings.path { return Ok(LanguageServerBinary { - path, - args: binary_settings.arguments, + path: path.clone(), + args: binary_settings.arguments.clone(), env: Some(worktree.shell_env()), }); } @@ -133,7 +173,7 @@ pub trait LanguageServer { Ok(_version) => { let bundle_path = worktree .which("bundle") - .ok_or("Unable to find 'bundle' command: e")?; + .ok_or_else(|| "Unable to find 'bundle' command".to_string())?; Ok(LanguageServerBinary { path: bundle_path, @@ -236,9 +276,7 @@ pub trait LanguageServer { #[cfg(test)] mod tests { - use crate::language_servers::language_server::FakeWorktree; - - use super::{LanguageServer, WorktreeLike}; + use super::{FakeWorktree, LanguageServer, WorktreeLike}; struct TestServer {} diff --git a/src/language_servers/mod.rs b/src/language_servers/mod.rs index 6360685..4a5c133 100644 --- a/src/language_servers/mod.rs +++ b/src/language_servers/mod.rs @@ -2,10 +2,12 @@ mod language_server; mod rubocop; mod ruby_lsp; mod solargraph; +mod sorbet; mod steep; pub use language_server::LanguageServer; pub use rubocop::*; pub use ruby_lsp::*; pub use solargraph::*; +pub use sorbet::*; pub use steep::*; diff --git a/src/language_servers/sorbet.rs b/src/language_servers/sorbet.rs new file mode 100644 index 0000000..ca0b579 --- /dev/null +++ b/src/language_servers/sorbet.rs @@ -0,0 +1,155 @@ +use super::{language_server::WorktreeLike, LanguageServer}; + +pub struct Sorbet {} + +impl LanguageServer for Sorbet { + const SERVER_ID: &str = "sorbet"; + const EXECUTABLE_NAME: &str = "srb"; + const GEM_NAME: &str = "sorbet"; + + fn get_executable_args(&self, worktree: &T) -> Vec { + let binary_settings = worktree + .lsp_binary_settings(Self::SERVER_ID) + .unwrap_or_default(); + + let default_args = vec![ + "tc".to_string(), + "--lsp".to_string(), + "--enable-experimental-lsp-document-highlight".to_string(), + ]; + + // test if sorbet/config is present + match worktree.read_text_file("sorbet/config") { + Ok(_) => { + // Config file exists, prefer custom arguments if available. + binary_settings + .and_then(|bs| bs.arguments) + .unwrap_or(default_args) + } + Err(_) => { + // gross, but avoid sorbet errors in a non-sorbet + // environment by using an empty config + vec![ + "tc".to_string(), + "--lsp".to_string(), + "--dir".to_string(), + "./".to_string(), + ] + } + } + } +} + +impl Sorbet { + pub fn new() -> Self { + Self {} + } +} + +#[cfg(test)] +mod tests { + use crate::language_servers::{ + language_server::{FakeWorktree, LspBinarySettings}, + LanguageServer, Sorbet, + }; + + #[test] + fn test_server_id() { + assert_eq!(Sorbet::SERVER_ID, "sorbet"); + } + + #[test] + fn test_executable_name() { + assert_eq!(Sorbet::EXECUTABLE_NAME, "srb"); + } + + #[test] + fn test_executable_args_no_config_file() { + let sorbet = Sorbet::new(); + let mut fake_worktree = FakeWorktree::new("/path/to/project".to_string()); + + fake_worktree.add_file( + "sorbet/config".to_string(), + Err("File not found".to_string()), + ); + fake_worktree.add_lsp_binary_setting(Sorbet::SERVER_ID.to_string(), Ok(None)); + + let expected_args_no_config = vec![ + "tc".to_string(), + "--lsp".to_string(), + "--dir".to_string(), + "./".to_string(), + ]; + assert_eq!( + sorbet.get_executable_args(&fake_worktree), + expected_args_no_config, + "Should use fallback arguments when sorbet/config is not found" + ); + } + + #[test] + fn test_executable_args_with_config_and_custom_settings() { + let sorbet = Sorbet::new(); + let mut fake_worktree = FakeWorktree::new("/path/to/project".to_string()); + + fake_worktree.add_file("sorbet/config".to_string(), Ok("--dir\n.".to_string())); + + let custom_args = vec!["--custom-arg1".to_string(), "value1".to_string()]; + fake_worktree.add_lsp_binary_setting( + Sorbet::SERVER_ID.to_string(), + Ok(Some(LspBinarySettings { + path: None, + arguments: Some(custom_args.clone()), + })), + ); + + assert_eq!( + sorbet.get_executable_args(&fake_worktree), + custom_args, + "Should use custom arguments when config and settings are present" + ); + } + + #[test] + fn test_executable_args_with_config_no_custom_settings() { + let sorbet = Sorbet::new(); + let mut fake_worktree = FakeWorktree::new("/path/to/project".to_string()); + + fake_worktree.add_file("sorbet/config".to_string(), Ok("--dir\n.".to_string())); + fake_worktree.add_lsp_binary_setting(Sorbet::SERVER_ID.to_string(), Ok(None)); + + let expected_default_args = vec![ + "tc".to_string(), + "--lsp".to_string(), + "--enable-experimental-lsp-document-highlight".to_string(), + ]; + assert_eq!( + sorbet.get_executable_args(&fake_worktree), + expected_default_args, + "Should use default arguments when config is present but no custom settings" + ); + } + + #[test] + fn test_executable_args_with_config_lsp_settings_is_empty_struct() { + let sorbet = Sorbet::new(); + let mut fake_worktree = FakeWorktree::new("/path/to/project".to_string()); + + fake_worktree.add_file("sorbet/config".to_string(), Ok("--dir\n.".to_string())); + fake_worktree.add_lsp_binary_setting( + Sorbet::SERVER_ID.to_string(), + Ok(Some(LspBinarySettings::default())), + ); + + let expected_default_args = vec![ + "tc".to_string(), + "--lsp".to_string(), + "--enable-experimental-lsp-document-highlight".to_string(), + ]; + assert_eq!( + sorbet.get_executable_args(&fake_worktree), + expected_default_args, + "Should use default arguments when config is present and LSP settings have no arguments" + ); + } +} diff --git a/src/ruby.rs b/src/ruby.rs index a5d911f..dbc9bb3 100644 --- a/src/ruby.rs +++ b/src/ruby.rs @@ -3,7 +3,7 @@ mod command_executor; mod gemset; mod language_servers; -use language_servers::{LanguageServer, Rubocop, RubyLsp, Solargraph, Steep}; +use language_servers::{LanguageServer, Rubocop, RubyLsp, Solargraph, Sorbet, Steep}; use zed_extension_api::{self as zed}; #[derive(Default)] @@ -11,6 +11,7 @@ struct RubyExtension { solargraph: Option, ruby_lsp: Option, rubocop: Option, + sorbet: Option, steep: Option, } @@ -37,6 +38,10 @@ impl zed::Extension for RubyExtension { let rubocop = self.rubocop.get_or_insert_with(Rubocop::new); rubocop.language_server_command(language_server_id, worktree) } + Sorbet::SERVER_ID => { + let sorbet = self.sorbet.get_or_insert_with(Sorbet::new); + sorbet.language_server_command(language_server_id, worktree) + } Steep::SERVER_ID => { let steep = self.steep.get_or_insert_with(Steep::new); steep.language_server_command(language_server_id, worktree)