Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ license = "Apache-2.0"
path = "src/ruby.rs"
crate-type = ["cdylib"]

[features]
default = []
command_api = []

[dependencies]
regex = "1.11.1"
serde_json = "1.0"
Expand Down
24 changes: 0 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,3 @@
# Ruby extension for Zed

[Documentation](https://zed.dev/docs/languages/ruby)

## Command-free LSP build

The default build does not run extension-side process commands for Ruby LSP
startup. It uses configured `lsp.<server>.binary.path` values first, then falls
back to `worktree.which`. If `use_bundler` is enabled, it launches through
`bundle exec <server>` without probing Bundler.

```sh
cargo test
```

To enable the command API path for project gem detection and extension-managed
language server/debug gem installation, build with:

```sh
cargo test --features command_api
```

This is not a replacement for fixing Zed's command spawning behavior:
https://github.com/zed-industries/zed/issues/57170. The command-free profile
expects `bundle` or the language server executable to be available from the
project environment. Debugging expects `rdbg` to be available from that same
environment.
2 changes: 0 additions & 2 deletions src/command_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,9 @@ pub trait CommandExecutor {

/// An implementation of `CommandExecutor` that executes commands
/// using the `zed_extension_api::Command`.
#[cfg(feature = "command_api")]
#[derive(Clone)]
pub struct RealCommandExecutor;

#[cfg(feature = "command_api")]
impl CommandExecutor for RealCommandExecutor {
fn execute(
&self,
Expand Down
174 changes: 0 additions & 174 deletions src/language_servers/language_server.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
#[cfg(test)]
use std::collections::HashMap;

#[cfg(feature = "command_api")]
use crate::{
bundler::Bundler,
command_executor::RealCommandExecutor,
gemset::{versioned_gem_home, Gemset},
};
#[cfg(feature = "command_api")]
use std::path::PathBuf;
use zed_extension_api::{self as zed};

Expand All @@ -32,8 +30,6 @@ pub trait WorktreeLike {
fn shell_env(&self) -> Vec<(String, String)>;
fn read_text_file(&self, path: &str) -> Result<String, String>;
fn lsp_binary_settings(&self, server_id: &str) -> Result<Option<LspBinarySettings>, String>;
#[cfg(any(test, not(feature = "command_api")))]
fn use_bundler(&self, server_id: &str) -> Result<Option<bool>, String>;
fn which(&self, name: &str) -> Option<String>;
}

Expand All @@ -60,16 +56,6 @@ impl WorktreeLike for zed::Worktree {
}
}

#[cfg(any(test, not(feature = "command_api")))]
fn use_bundler(&self, server_id: &str) -> Result<Option<bool>, String> {
zed::settings::LspSettings::for_worktree(server_id, self).map(|lsp_settings| {
lsp_settings
.settings
.as_ref()
.and_then(|settings| settings["use_bundler"].as_bool())
})
}

fn which(&self, name: &str) -> Option<String> {
zed::Worktree::which(self, name)
}
Expand All @@ -81,7 +67,6 @@ pub struct FakeWorktree {
shell_env: Vec<(String, String)>,
files: HashMap<String, Result<String, String>>,
lsp_binary_settings_map: HashMap<String, Result<Option<LspBinarySettings>, String>>,
use_bundler_map: HashMap<String, Result<Option<bool>, String>>,
which_map: HashMap<String, Option<String>>,
}

Expand All @@ -93,7 +78,6 @@ impl FakeWorktree {
shell_env: Vec::new(),
files: HashMap::new(),
lsp_binary_settings_map: HashMap::new(),
use_bundler_map: HashMap::new(),
which_map: HashMap::new(),
}
}
Expand All @@ -110,10 +94,6 @@ impl FakeWorktree {
self.lsp_binary_settings_map.insert(server_id, settings);
}

pub fn set_use_bundler(&mut self, server_id: String, value: Result<Option<bool>, String>) {
self.use_bundler_map.insert(server_id, value);
}

pub fn set_which(&mut self, name: String, result: Option<String>) {
self.which_map.insert(name, result);
}
Expand Down Expand Up @@ -143,13 +123,6 @@ impl WorktreeLike for FakeWorktree {
.unwrap_or(Ok(None))
}

fn use_bundler(&self, server_id: &str) -> Result<Option<bool>, String> {
self.use_bundler_map
.get(server_id)
.cloned()
.unwrap_or(Ok(None))
}

fn which(&self, name: &str) -> Option<String> {
self.which_map.get(name).cloned().flatten()
}
Expand Down Expand Up @@ -193,12 +166,6 @@ pub trait LanguageServer {
language_server_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> zed::Result<LanguageServerBinary> {
#[cfg(not(feature = "command_api"))]
{
self.command_free_language_server_binary(language_server_id.as_ref(), worktree)
}

#[cfg(feature = "command_api")]
{
let lsp_settings =
zed::settings::LspSettings::for_worktree(language_server_id.as_ref(), worktree)?;
Expand Down Expand Up @@ -252,56 +219,6 @@ pub trait LanguageServer {
}
}

#[cfg(any(test, not(feature = "command_api")))]
fn command_free_language_server_binary<T: WorktreeLike>(
&self,
server_id: &str,
worktree: &T,
) -> zed::Result<LanguageServerBinary> {
if let Some(binary_settings) = worktree.lsp_binary_settings(server_id)? {
if let Some(path) = binary_settings.path {
return Ok(LanguageServerBinary {
path,
args: binary_settings.arguments,
env: Some(worktree.shell_env()),
});
}
}

let use_bundler = worktree
.use_bundler(server_id)?
.unwrap_or_else(Self::default_use_bundler);

if use_bundler {
if let Some(bundle_path) = worktree.which("bundle") {
return Ok(LanguageServerBinary {
path: bundle_path,
args: Some(
vec!["exec".into(), Self::EXECUTABLE_NAME.into()]
.into_iter()
.chain(self.get_executable_args(worktree))
.collect(),
),
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(format!(
"Unable to find 'bundle' or '{}' command for {server_id}. Install one in the project environment or configure lsp.{server_id}.binary.path.",
Self::EXECUTABLE_NAME
))
}

#[cfg(feature = "command_api")]
fn try_find_on_path_or_extension_gemset(
&self,
language_server_id: &zed::LanguageServerId,
Expand All @@ -318,7 +235,6 @@ pub trait LanguageServer {
}
}

#[cfg(feature = "command_api")]
fn extension_gemset_language_server_binary(
&self,
language_server_id: &zed::LanguageServerId,
Expand Down Expand Up @@ -454,94 +370,4 @@ mod tests {
let mock_worktree = FakeWorktree::new("/path/to/project".to_string());
assert_eq!(mock_worktree.shell_env(), Vec::<(String, String)>::new());
}

#[test]
fn test_command_free_uses_bundle_exec_when_use_bundler_enabled() {
let test_server = TestServer::new();
let mut mock_worktree = FakeWorktree::new("/path/to/project".to_string());
mock_worktree.set_use_bundler(TestServer::SERVER_ID.to_string(), Ok(Some(true)));
mock_worktree.set_which("bundle".to_string(), Some("/bin/bundle".to_string()));

let binary = test_server
.command_free_language_server_binary(TestServer::SERVER_ID, &mock_worktree)
.expect("command-free resolver should find bundle");

assert_eq!(binary.path, "/bin/bundle");
assert_eq!(
binary.args,
Some(vec![
"exec".to_string(),
"test-exe".to_string(),
"--test-arg".to_string()
])
);
}

#[test]
fn test_command_free_falls_back_to_executable_when_bundle_missing() {
let test_server = TestServer::new();
let mut mock_worktree = FakeWorktree::new("/path/to/project".to_string());
mock_worktree.set_use_bundler(TestServer::SERVER_ID.to_string(), Ok(Some(true)));
mock_worktree.set_which("bundle".to_string(), None);
mock_worktree.set_which("test-exe".to_string(), Some("/bin/test-exe".to_string()));

let binary = test_server
.command_free_language_server_binary(TestServer::SERVER_ID, &mock_worktree)
.expect("command-free resolver should fall back to executable");

assert_eq!(binary.path, "/bin/test-exe");
assert_eq!(binary.args, Some(vec!["--test-arg".to_string()]));
}

#[test]
fn test_command_free_uses_configured_binary_before_bundler() {
let test_server = TestServer::new();
let mut mock_worktree = FakeWorktree::new("/path/to/project".to_string());
mock_worktree.set_use_bundler(TestServer::SERVER_ID.to_string(), Ok(Some(true)));
mock_worktree.add_lsp_binary_setting(
TestServer::SERVER_ID.to_string(),
Ok(Some(super::LspBinarySettings {
path: Some("/custom/test-exe".to_string()),
arguments: Some(vec!["--custom".to_string()]),
})),
);

let binary = test_server
.command_free_language_server_binary(TestServer::SERVER_ID, &mock_worktree)
.expect("command-free resolver should use configured binary");

assert_eq!(binary.path, "/custom/test-exe");
assert_eq!(binary.args, Some(vec!["--custom".to_string()]));
}

#[test]
fn test_command_free_uses_path_lookup_when_use_bundler_disabled() {
let test_server = TestServer::new();
let mut mock_worktree = FakeWorktree::new("/path/to/project".to_string());
mock_worktree.set_use_bundler(TestServer::SERVER_ID.to_string(), Ok(Some(false)));
mock_worktree.set_which("test-exe".to_string(), Some("/bin/test-exe".to_string()));

let binary = test_server
.command_free_language_server_binary(TestServer::SERVER_ID, &mock_worktree)
.expect("command-free resolver should find server executable");

assert_eq!(binary.path, "/bin/test-exe");
assert_eq!(binary.args, Some(vec!["--test-arg".to_string()]));
}

#[test]
fn test_command_free_missing_executable_errors() {
let test_server = TestServer::new();
let mut mock_worktree = FakeWorktree::new("/path/to/project".to_string());
mock_worktree.set_use_bundler(TestServer::SERVER_ID.to_string(), Ok(Some(true)));
mock_worktree.set_which("bundle".to_string(), None);
mock_worktree.set_which("test-exe".to_string(), None);

let error = test_server
.command_free_language_server_binary(TestServer::SERVER_ID, &mock_worktree)
.expect_err("command-free resolver should fail when executable is missing");

assert!(error.contains("Unable to find 'bundle' or 'test-exe' command"));
assert!(error.contains("lsp.test-server.binary.path"));
}
}
17 changes: 0 additions & 17 deletions src/ruby.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
#[cfg(feature = "command_api")]
mod bundler;
#[cfg(feature = "command_api")]
mod command_executor;
#[cfg(feature = "command_api")]
mod gemset;
mod language_servers;

use std::collections::HashMap;
#[cfg(feature = "command_api")]
use std::path::PathBuf;

#[cfg(feature = "command_api")]
use bundler::Bundler;
#[cfg(feature = "command_api")]
use command_executor::RealCommandExecutor;
#[cfg(feature = "command_api")]
use gemset::{versioned_gem_home, Gemset};
use language_servers::{
FuzzyRubyServer, Herb, Kanayago, LanguageServer, Rubocop, RubyLsp, Solargraph, Sorbet, Steep,
Expand Down Expand Up @@ -149,7 +142,6 @@ impl zed::Extension for RubyExtension {
_: Option<String>,
worktree: &Worktree,
) -> Result<DebugAdapterBinary, String> {
#[cfg(feature = "command_api")]
let (command, mut arguments) = {
let shell_env = worktree.shell_env();
let env_vars: Vec<(&str, &str)> = shell_env
Expand Down Expand Up @@ -181,15 +173,6 @@ impl zed::Extension for RubyExtension {
}
};

#[cfg(not(feature = "command_api"))]
let (command, mut arguments) = if let Some(path) = worktree.which(&adapter_name) {
(path, Vec::new())
} else {
return Err(format!(
"Unable to find '{adapter_name}' command in the project environment"
));
};

let tcp_connection = config.tcp_connection.unwrap_or(TcpArgumentsTemplate {
port: None,
host: None,
Expand Down