Skip to content
Draft
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
29 changes: 13 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,23 @@

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

## Command-free LSP build
## Ruby command resolution

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.
The extension uses configured `lsp.<server>.binary.path` values first. If
`use_bundler` is enabled, it checks Bundler and launches through
`bundle exec <server>`. Otherwise it falls back to `worktree.which` or the
extension-managed gemset.

```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.
https://github.com/zed-industries/zed/issues/57170. Debugging expects `rdbg` to
be available from the project environment.

On macOS, extension-side Ruby, Bundler, and gem probes run through
`/bin/sh -c 'exec "$0" "$@"' ...`. Bundler-mode LSP startup likewise uses shell
resolution so version managers (rbenv, chruby, mise, asdf) are honored. Gemset
LSP startup runs `ruby <gemset-binstub>` from the worktree root so the project
Ruby is activated; DAP startup uses Zed worktree command resolution directly.
39 changes: 39 additions & 0 deletions extension.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,40 +58,79 @@ commit = "c70c1de07dedd532089c0c90835c8ed9fa694f5c"
repository = "https://github.com/joker1007/tree-sitter-rbs"
commit = "5282e2f36d4109f5315c1d9486b5b0c2044622bb"

# Each command below appears twice: once for direct execution (non-macOS), and once wrapped via
# /bin/sh (macOS). The shell-wrapper form is required on macOS so version managers (rbenv, chruby,
# mise, asdf) can resolve executables from the correct PATH at spawn time. Both entries must stay
# in sync when a command's args change in code.
[[capabilities]]
kind = "process:exec"
command = "gem"
args = ["install", "--norc", "--no-user-install", "--no-format-executable", "--no-document", "*"]

[[capabilities]]
kind = "process:exec"
command = "/bin/sh"
args = ["-c", "exec \"$0\" \"$@\"", "gem", "install", "--norc", "--no-user-install", "--no-format-executable", "--no-document", "*"]

[[capabilities]]
kind = "process:exec"
command = "gem"
args = ["uninstall", "--norc", "*", "--version", "*"]

[[capabilities]]
kind = "process:exec"
command = "/bin/sh"
args = ["-c", "exec \"$0\" \"$@\"", "gem", "uninstall", "--norc", "*", "--version", "*"]

[[capabilities]]
kind = "process:exec"
command = "gem"
args = ["list", "--norc", "--exact", "*"]

[[capabilities]]
kind = "process:exec"
command = "/bin/sh"
args = ["-c", "exec \"$0\" \"$@\"", "gem", "list", "--norc", "--exact", "*"]

[[capabilities]]
kind = "process:exec"
command = "bundle"
args = ["info", "--version", "*"]

[[capabilities]]
kind = "process:exec"
command = "/bin/sh"
args = ["-c", "exec \"$0\" \"$@\"", "bundle", "info", "--version", "*"]

[[capabilities]]
kind = "process:exec"
command = "gem"
args = ["outdated", "--norc"]

[[capabilities]]
kind = "process:exec"
command = "/bin/sh"
args = ["-c", "exec \"$0\" \"$@\"", "gem", "outdated", "--norc"]

[[capabilities]]
kind = "process:exec"
command = "gem"
args = ["update", "--norc", "*"]

[[capabilities]]
kind = "process:exec"
command = "/bin/sh"
args = ["-c", "exec \"$0\" \"$@\"", "gem", "update", "--norc", "*"]

[[capabilities]]
kind = "process:exec"
command = "ruby"
args = ["--version"]

[[capabilities]]
kind = "process:exec"
command = "/bin/sh"
args = ["-c", "cd \"$0\" && command=\"$1\" && shift && exec \"$command\" \"$@\"", "*", "ruby", "--version"]

[debug_adapters.rdbg]
[debug_locators.ruby]
33 changes: 32 additions & 1 deletion src/bundler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,14 @@ impl<E: CommandExecutor> Bundler<E> {

let output = self
.command_executor
.execute("bundle", &full_args, &command_envs)
.execute_in_dir(
"bundle",
&full_args,
&command_envs,
self.working_dir.to_str().with_context(|| {
format!("Invalid working directory: {}", self.working_dir.display())
})?,
)
.map_err(|e| anyhow::anyhow!(e))?;

match output.status {
Expand Down Expand Up @@ -84,6 +91,7 @@ mod tests {
expected_command_name: Option<String>,
expected_args: Option<Vec<String>>,
expected_envs: Option<Vec<(String, String)>>,
expected_cwd: Option<String>,
}

struct MockCommandExecutor {
Expand All @@ -98,6 +106,7 @@ mod tests {
expected_command_name: None,
expected_args: None,
expected_envs: None,
expected_cwd: None,
}),
}
}
Expand All @@ -107,6 +116,7 @@ mod tests {
command_name: &str,
full_args: &[&str],
final_envs: &[(&str, &str)],
cwd: &str,
output: Result<Output, String>,
) {
let mut config = self.config.borrow_mut();
Expand All @@ -118,6 +128,7 @@ mod tests {
.map(|&(k, v)| (k.to_string(), v.to_string()))
.collect(),
);
config.expected_cwd = Some(cwd.to_string());
config.output_to_return = Some(output);
}
}
Expand Down Expand Up @@ -149,6 +160,23 @@ mod tests {
"MockCommandExecutor: output_to_return was not set or already consumed for the test",
)
}

fn execute_in_dir(
&self,
command_name: &str,
args: &[&str],
envs: &[(&str, &str)],
cwd: &str,
) -> Result<Output, String> {
{
let config = self.config.borrow();
if let Some(expected_cwd) = &config.expected_cwd {
assert_eq!(cwd, expected_cwd, "Mock: Cwd mismatch");
}
}

self.execute(command_name, args, envs)
}
}

fn create_mock_executor_for_success(
Expand All @@ -165,6 +193,7 @@ mod tests {
"bundle",
&["info", "--version", gem],
&[("BUNDLE_GEMFILE", &gemfile_path)],
dir,
Ok(Output {
status: Some(0),
stdout: version.as_bytes().to_vec(),
Expand Down Expand Up @@ -198,6 +227,7 @@ mod tests {
"bundle",
&["info", "--version", gem_name],
&[("BUNDLE_GEMFILE", &gemfile_path)],
"test_dir",
Ok(Output {
status: Some(1),
stdout: Vec::new(),
Expand Down Expand Up @@ -237,6 +267,7 @@ mod tests {
"bundle",
&["info", "--version", gem_name],
&[("BUNDLE_GEMFILE", &gemfile_path)],
"test_dir",
Err(specific_error_msg.to_string()),
);

Expand Down
Loading
Loading