From 3bc38c1e3395bed1665ca2827e79f63429244a9d Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Fri, 1 Nov 2024 08:30:06 -0700 Subject: [PATCH 1/2] feat: add sorbet Ruby LSP server option --- extension.toml | 4 +++ src/language_servers/mod.rs | 2 ++ src/language_servers/sorbet.rs | 53 ++++++++++++++++++++++++++++++++++ src/ruby.rs | 7 ++++- 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 src/language_servers/sorbet.rs 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/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..e69af4a --- /dev/null +++ b/src/language_servers/sorbet.rs @@ -0,0 +1,53 @@ +use super::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() -> Vec { + [ + "tc", + "--lsp", + "--enable-experimental-lsp-document-highlight", + ] + .iter() + .map(|s| s.to_string()) + .collect() + } +} + +impl Sorbet { + pub fn new() -> Self { + Self {} + } +} + +#[cfg(test)] +mod tests { + use crate::language_servers::{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() { + assert_eq!( + Sorbet::get_executable_args(), + vec![ + "tc", + "--lsp", + "--enable-experimental-lsp-document-highlight" + ] + ); + } +} 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) From 52a76d623e1bccf9f79d6ec754158868e4726443 Mon Sep 17 00:00:00 2001 From: Vitaly Slobodin Date: Sat, 7 Jun 2025 20:14:41 +0200 Subject: [PATCH 2/2] sorbet: Resolve merge conflicts --- src/language_servers/language_server.rs | 72 ++++++++++--- src/language_servers/sorbet.rs | 138 ++++++++++++++++++++---- 2 files changed, 175 insertions(+), 35 deletions(-) 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/sorbet.rs b/src/language_servers/sorbet.rs index e69af4a..ca0b579 100644 --- a/src/language_servers/sorbet.rs +++ b/src/language_servers/sorbet.rs @@ -1,4 +1,4 @@ -use super::LanguageServer; +use super::{language_server::WorktreeLike, LanguageServer}; pub struct Sorbet {} @@ -7,15 +7,36 @@ impl LanguageServer for Sorbet { const EXECUTABLE_NAME: &str = "srb"; const GEM_NAME: &str = "sorbet"; - fn get_executable_args() -> Vec { - [ - "tc", - "--lsp", - "--enable-experimental-lsp-document-highlight", - ] - .iter() - .map(|s| s.to_string()) - .collect() + 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(), + ] + } + } } } @@ -27,7 +48,10 @@ impl Sorbet { #[cfg(test)] mod tests { - use crate::language_servers::{LanguageServer, Sorbet}; + use crate::language_servers::{ + language_server::{FakeWorktree, LspBinarySettings}, + LanguageServer, Sorbet, + }; #[test] fn test_server_id() { @@ -40,14 +64,92 @@ mod tests { } #[test] - fn test_executable_args() { + 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(), - vec![ - "tc", - "--lsp", - "--enable-experimental-lsp-document-highlight" - ] + 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" + ); + } }