diff --git a/crates/lsp/src/config.rs b/crates/lsp/src/config.rs index 71cb9cac0..17e01c3e7 100644 --- a/crates/lsp/src/config.rs +++ b/crates/lsp/src/config.rs @@ -32,10 +32,16 @@ pub enum LanguageId { JavaScriptReact, C, Cpp, + Ruby, } impl LanguageId { pub fn from_path(path: &Path) -> Option { + if let Some("Gemfile" | "Rakefile" | "Guardfile" | "Capfile") = + path.file_name().and_then(|n| n.to_str()) + { + return Some(Self::Ruby); + } let extn = path.extension()?; match extn.to_str()? { "rs" => Some(Self::Rust), @@ -52,6 +58,7 @@ impl LanguageId { // compile_commands.json is present, clangd will use the correct language // regardless of the languageId we send. "h" | "H" | "hh" | "hpp" | "hxx" => Some(Self::Cpp), + "rb" | "rbw" | "rake" | "gemspec" | "ru" => Some(Self::Ruby), _ => None, } } @@ -69,6 +76,7 @@ impl LanguageId { LanguageId::JavaScriptReact => "javascriptreact", LanguageId::C => "c", LanguageId::Cpp => "cpp", + LanguageId::Ruby => "ruby", } } @@ -83,6 +91,7 @@ impl LanguageId { | LanguageId::JavaScript | LanguageId::JavaScriptReact => LSPServerType::TypeScriptLanguageServer, LanguageId::C | LanguageId::Cpp => LSPServerType::Clangd, + LanguageId::Ruby => LSPServerType::RubyLsp, } } } diff --git a/crates/lsp/src/config_tests.rs b/crates/lsp/src/config_tests.rs index e4ec2dd22..152c2dd7c 100644 --- a/crates/lsp/src/config_tests.rs +++ b/crates/lsp/src/config_tests.rs @@ -1,8 +1,10 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use lsp_types::Uri; use crate::config::{lsp_uri_to_path, path_to_lsp_uri}; +use crate::supported_servers::LSPServerType; +use crate::LanguageId; // Unix-specific tests use Unix paths #[cfg(not(windows))] @@ -216,3 +218,42 @@ fn test_path_to_lsp_uri_rejects_relative_path() { assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("must be absolute")); } + +#[test] +fn test_ruby_language_id_from_extension() { + for ext in ["rb", "rbw", "rake", "gemspec", "ru"] { + let path = PathBuf::from(format!("foo.{ext}")); + assert_eq!( + LanguageId::from_path(&path), + Some(LanguageId::Ruby), + "extension .{ext} should map to Ruby" + ); + } +} + +#[test] +fn test_ruby_language_id_from_filename() { + for name in ["Gemfile", "Rakefile", "Guardfile", "Capfile"] { + let path = Path::new(name); + assert_eq!( + LanguageId::from_path(path), + Some(LanguageId::Ruby), + "filename {name} should map to Ruby" + ); + } +} + +#[test] +fn test_ruby_server_type() { + assert_eq!(LanguageId::Ruby.server_type(), LSPServerType::RubyLsp); +} + +#[test] +fn test_ruby_lsp_binary_name() { + assert_eq!(LSPServerType::RubyLsp.binary_name(), "ruby-lsp"); +} + +#[test] +fn test_ruby_lsp_language_name() { + assert_eq!(LSPServerType::RubyLsp.language_name(), "Ruby"); +} diff --git a/crates/lsp/src/servers/mod.rs b/crates/lsp/src/servers/mod.rs index 32d836768..27fe8e54f 100644 --- a/crates/lsp/src/servers/mod.rs +++ b/crates/lsp/src/servers/mod.rs @@ -1,5 +1,6 @@ pub mod clangd; pub mod go; pub mod pyright; +pub mod ruby_lsp; pub mod rust; pub mod typescript_language_server; diff --git a/crates/lsp/src/servers/ruby_lsp.rs b/crates/lsp/src/servers/ruby_lsp.rs new file mode 100644 index 000000000..a823a2555 --- /dev/null +++ b/crates/lsp/src/servers/ruby_lsp.rs @@ -0,0 +1,94 @@ +use std::path::Path; +use std::sync::Arc; + +use crate::language_server_candidate::{LanguageServerCandidate, LanguageServerMetadata}; +use crate::CommandBuilder; +use async_trait::async_trait; + +#[cfg_attr(not(feature = "local_fs"), allow(dead_code))] +pub struct RubyLspCandidate { + #[allow(dead_code)] + client: Arc, +} + +impl RubyLspCandidate { + pub fn new(client: Arc) -> Self { + Self { client } + } +} + +#[async_trait] +#[cfg(feature = "local_fs")] +impl LanguageServerCandidate for RubyLspCandidate { + async fn should_suggest_for_repo(&self, path: &Path, _executor: &CommandBuilder) -> bool { + if path.join("Gemfile").exists() + || path.join("Rakefile").exists() + || path.join(".ruby-version").exists() + || path.join("config.ru").exists() + { + return true; + } + + std::fs::read_dir(path) + .map(|entries| { + entries.flatten().any(|entry| { + entry.path().extension().and_then(|s| s.to_str()) == Some("gemspec") + }) + }) + .unwrap_or(false) + } + + async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool { + false + } + + async fn is_installed_on_path(&self, executor: &CommandBuilder) -> bool { + executor + .command("ruby-lsp") + .arg("--version") + .output() + .await + .map(|o| o.status.success()) + .unwrap_or(false) + } + + async fn install( + &self, + _metadata: LanguageServerMetadata, + _executor: &CommandBuilder, + ) -> anyhow::Result<()> { + anyhow::bail!("Install ruby-lsp manually: `gem install ruby-lsp`") + } + + async fn fetch_latest_server_metadata(&self) -> anyhow::Result { + anyhow::bail!("Auto-install not supported; install via `gem install ruby-lsp`") + } +} + +#[async_trait] +#[cfg(not(feature = "local_fs"))] +impl LanguageServerCandidate for RubyLspCandidate { + async fn should_suggest_for_repo(&self, _path: &Path, _executor: &CommandBuilder) -> bool { + false + } + + async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool { + false + } + + async fn is_installed_on_path(&self, _executor: &CommandBuilder) -> bool { + false + } + + async fn install( + &self, + _metadata: LanguageServerMetadata, + _executor: &CommandBuilder, + ) -> anyhow::Result<()> { + todo!() + } + + async fn fetch_latest_server_metadata(&self) -> anyhow::Result { + todo!() + } +} diff --git a/crates/lsp/src/supported_servers.rs b/crates/lsp/src/supported_servers.rs index 80ba19253..8bf87a681 100644 --- a/crates/lsp/src/supported_servers.rs +++ b/crates/lsp/src/supported_servers.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use crate::servers::clangd::ClangdCandidate; use crate::servers::go::GoPlsCandidate; use crate::servers::pyright::PyrightCandidate; +use crate::servers::ruby_lsp::RubyLspCandidate; use crate::servers::rust::RustAnalyzerCandidate; use crate::servers::typescript_language_server::TypeScriptLanguageServerCandidate; #[cfg(not(target_arch = "wasm32"))] @@ -42,6 +43,7 @@ pub enum LSPServerType { Pyright, TypeScriptLanguageServer, Clangd, + RubyLsp, } /// Provides server-specific configuration for each LSP server type. @@ -109,6 +111,7 @@ impl LSPServerType { binary_path: path, prepend_args: vec![], }), + LSPServerType::RubyLsp => None, } } @@ -132,6 +135,7 @@ impl LSPServerType { LSPServerType::Pyright => "pyright-langserver", LSPServerType::TypeScriptLanguageServer => "typescript-language-server", LSPServerType::Clangd => "clangd", + LSPServerType::RubyLsp => "ruby-lsp", } } @@ -139,7 +143,10 @@ impl LSPServerType { #[cfg(not(target_arch = "wasm32"))] fn args(&self) -> Vec<&'static str> { match self { - LSPServerType::RustAnalyzer | LSPServerType::GoPls | LSPServerType::Clangd => vec![], + LSPServerType::RustAnalyzer + | LSPServerType::GoPls + | LSPServerType::Clangd + | LSPServerType::RubyLsp => vec![], LSPServerType::Pyright | LSPServerType::TypeScriptLanguageServer => vec!["--stdio"], } } @@ -154,6 +161,7 @@ impl LSPServerType { LSPServerType::Pyright => vec!["--stdio"], LSPServerType::TypeScriptLanguageServer => vec!["--stdio"], LSPServerType::Clangd => vec![], + LSPServerType::RubyLsp => vec![], } } @@ -172,6 +180,7 @@ impl LSPServerType { ] } LSPServerType::Clangd => vec![LanguageId::C, LanguageId::Cpp], + LSPServerType::RubyLsp => vec![LanguageId::Ruby], } } @@ -205,6 +214,7 @@ impl LSPServerType { Box::new(TypeScriptLanguageServerCandidate::new(client)) } LSPServerType::Clangd => Box::new(ClangdCandidate::new(client)), + LSPServerType::RubyLsp => Box::new(RubyLspCandidate::new(client)), } }