Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ruby_runtime: Initial implementation #25350

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

vitallium
Copy link
Contributor

@vitallium vitallium commented Feb 21, 2025

Closes #25099

This is a proof of concept for providing Ruby runtime to extensions through Zed extensions API.

The idea

In the spirit of the node_runtime which provides limited methods for working with Node.js and npm, this PR is about introducing the ruby_runtime - a runtime for providing limited methods for working with Ruby. This is a proof of concept only.

How it works

In the same way as Zed does for node.js, the Ruby runtime relies on the correct environment when Zed starts

When an extension installs a Ruby gem, the Ruby runtime uses the detected Ruby version and installs the requested gem into the extenstion's working directory. The installed gem is not installed into the user's gemset to avoid activation errors like can't activate date (=3.6.3), already activated date-3.6.6] (Gem::Exception). Additionally, that method does not alter the user-installed gems. This is achieved by setting the GEM_PATH environment variable to the extension directory for all operations.

In the same way as Node runtime does, the Ruby runtime provides the following functions:

  • binary_path returns path to the Ruby binary, for example, /Users/vslobodin/.local/share/mise/installs/ruby/3.4.1/bin/ruby.

  • gems-latest-version(gem-name: string) Returns the latest version of the given Ruby gem name via gem-name.

  • gems-installed-version(gem-name: string) Returns the installed version of the given Ruby gem, if it exists.

  • gems-install-gem(gem-name: string, version: string, binaries: list<string>) Installs the specified version of a Ruby gem by its name. Plus, wrappes its binaries with a special shell wrapper that sets GEM_PATH before running the gem. This is required to run a Gem outside of the user gemset:

    For instance, a Ruby extension installs the solargraph gem. After installing it, if the binaries param was passed, the Ruby runtime creates wrappers for each given binary with the following content:

    #!/usr/bin/env bash
    export GEM_PATH="{gem_path}:$GEM_PATH"
    exec "{exec_path}" "$@"

    where:

    • gem_path path to the extension working dir.
    • exec_path path to the actual Ruby gem binary.

    For solargraph this wrapper will be:

    #!/usr/bin/env bash
    export GEM_PATH="/Users/vslobodin/Library/Application Support/Zed/extensions/work/ruby:$GEM_PATH"
    exec "/Users/vslobodin/Library/Application Support/Zed/extensions/work/ruby/bin/solargraph" "$@"
  • bundle-installed-version(gem-name: string) Not implemented but the idea of this method is to check if the given Ruby gem is available in the worktree Gemfile.

Notes about the implementation

  • SystemRubyRuntime - the currently available Ruby runtime which should point to a Ruby version that is activated in a worktree. A shell-based activation of the Ruby version is necessary to ensure the Ruby runtime uses the correct Ruby version.
  • UnavailableRubyRuntime - unavailale Ruby runtime with stubbed methods.

See also my comments please.

Known limitations and issues

  1. The Ruby runtime relies on the correct Ruby version being activated in a worktree. In theory, there could be an issue if detection of a Ruby version fails.

Possible improvements and ideas

  1. If the system Ruby version is not detected or its version is too low, macOS ships with Ruby 2.6.10 out of the box, and this version is unmaintained.
  2. Implement support for activating the correct Ruby version for most popular Ruby version managers. Ruby LSP for VSCode does that and that can be served as a reference.

References

Release Notes:

  • N/A or Added/Fixed/Improved ...

@cla-bot cla-bot bot added the cla-signed The user has signed the Contributor License Agreement label Feb 21, 2025
@vitallium vitallium changed the title ruby_runtime: draft ruby_runtime: initial implementation Feb 21, 2025
@vitallium vitallium changed the title ruby_runtime: initial implementation ruby_runtime: Initial implementation Feb 21, 2025
@ConradIrwin ConradIrwin self-assigned this Feb 21, 2025
@vitallium vitallium force-pushed the vs/ruby-runtime branch 2 times, most recently from 7a47372 to a9d182a Compare February 21, 2025 18:59
Comment on lines +146 to +155
output
.lines()
.find(|line| line.starts_with(name))
.and_then(|line| {
line.rfind('(')
.and_then(|start| line.rfind(')').map(|end| &line[start + 1..end]))
})
.map(|versions| versions.split(", ").map(String::from).collect())
.ok_or_else(|| anyhow!("Failed to parse gem list output."))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, the gem command lacks any structured output so this parses the output as-is. For instance, for gem list --remote --all ruby-lsp the output is:


*** REMOTE GEMS ***

ruby-lsp (0.23.11, 0.23.10, 0.23.9, 0.23.8, 0.23.7, 0.23.6, 0.23.5, 0.23.4, 0.23.3, 0.23.2, 0.23.1, 0.23.0, 0.22.1, 0.22.0, 0.21.3, 0.21.2, 0.21.1, 0.21.0, 0.20.1, 0.20.0, 0.19.1, 0.19.0, 0.18.4, 0.18.3, 0.18.2, 0.18.1, 0.18.0, 0.17.17, 0.17.16, 0.17.15, 0.17.14, 0.17.13, 0.17.12, 0.17.11, 0.17.10, 0.17.9, 0.17.8, 0.17.7, 0.17.6, 0.17.5, 0.17.4, 0.17.3, 0.17.2, 0.17.1, 0.17.0, 0.16.7, 0.16.6, 0.16.5, 0.16.4, 0.16.3, 0.16.2, 0.16.1, 0.16.0, 0.15.0, 0.14.6, 0.14.5, 0.14.4, 0.14.3, 0.14.2, 0.14.1, 0.14.0, 0.13.4, 0.13.3, 0.13.2, 0.13.1, 0.13.0, 0.12.5, 0.12.4, 0.12.3, 0.12.2, 0.12.1, 0.12.0, 0.11.2, 0.11.1, 0.11.0, 0.10.1, 0.10.0, 0.9.4, 0.9.3, 0.9.2, 0.9.1, 0.9.0, 0.8.1, 0.8.0, 0.7.6, 0.7.5, 0.7.4, 0.7.3, 0.7.2, 0.7.1, 0.7.0, 0.6.2, 0.6.1, 0.6.0, 0.5.1, 0.5.0, 0.4.5, 0.4.4, 0.4.3, 0.4.2, 0.4.1, 0.4.0, 0.3.8, 0.3.7, 0.3.6, 0.3.5, 0.3.4, 0.3.3, 0.3.2, 0.3.1, 0.3.0, 0.2.4, 0.2.3, 0.2.2, 0.2.1, 0.2.0, 0.1.0, 0.0.4, 0.0.3, 0.0.2, 0.0.1)
ruby-lsp-brakeman (0.0.1)
ruby-lsp-fabrication (0.3.0, 0.2.1, 0.2.0, 0.1.0)
ruby-lsp-factory_bot (0.3.0, 0.2.2, 0.2.1, 0.2.0, 0.1.0)
ruby-lsp-rails (0.4.0, 0.3.31, 0.3.30, 0.3.29, 0.3.28, 0.3.27, 0.3.26, 0.3.25, 0.3.24, 0.3.23, 0.3.22, 0.3.21, 0.3.20, 0.3.19, 0.3.18, 0.3.17, 0.3.16, 0.3.15, 0.3.14, 0.3.13, 0.3.12, 0.3.11, 0.3.10, 0.3.9, 0.3.8, 0.3.7, 0.3.6, 0.3.5, 0.3.4, 0.3.3, 0.3.2, 0.3.1, 0.3.0, 0.2.10, 0.2.9, 0.2.8, 0.2.7, 0.2.6, 0.2.5, 0.2.4, 0.2.3, 0.2.2, 0.2.1, 0.2.0, 0.1.0)
ruby-lsp-rails-factory-bot (0.5.0, 0.4.0, 0.3.0, 0.2.0, 0.1.0)
ruby-lsp-rake (0.3.4, 0.3.3, 0.3.2, 0.3.1, 0.3.0, 0.2.0, 0.1.5, 0.1.4, 0.1.3, 0.1.2, 0.1.1, 0.1.0)
ruby-lsp-ree (0.1.3, 0.1.2, 0.1.1, 0.1.0)
ruby-lsp-reek (0.3.1, 0.3.0, 0.2.0, 0.1.0)
ruby-lsp-rspec (0.1.22, 0.1.21, 0.1.20, 0.1.19, 0.1.18, 0.1.17, 0.1.16, 0.1.15, 0.1.14, 0.1.13, 0.1.12, 0.1.11, 0.1.10, 0.1.9, 0.1.8, 0.1.7, 0.1.6, 0.1.5, 0.1.4, 0.1.3, 0.1.2, 0.1.1, 0.1.0)
ruby-lsp-rubyfmt (0.1.0)
ruby-lsp-shoulda-context (0.4.9, 0.4.8, 0.4.7, 0.4.6, 0.4.5, 0.4.4, 0.4.3, 0.4.2, 0.4.1, 0.4.0, 0.3.3, 0.3.2, 0.3.1, 0.3.0, 0.2.0, 0.1.0)

Probably using the exact match via the --exact command line option is a good idea.

@@ -497,6 +497,52 @@ impl nodejs::Host for WasmState {
}
}

impl ruby::Host for WasmState {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This definitely should be in the next version of the Extension API, but this part of Zed is still a mystery to me. I tried the simplest approach by copying and pasting but failed.

@maxdeviant
Copy link
Member

@vitallium Just to set expectations, I think it's highly unlikely that we end up shipping a Ruby runtime with Zed.

I think a more promising direction would be exposing a process API to extensions to allow them to do this sort of thing themselves.

I have a start of one in #25399.

And I started a branch to play around with it in the Ruby extension: https://github.com/zed-extensions/ruby/blob/67d8485778f74a10909cd07644be27c5088dbfe9/src/language_servers/language_server.rs#L43-L47

Haven't wired up anything Ruby-specific yet, but I did confirm that the process execution is working.

@vitallium
Copy link
Contributor Author

@vitallium Just to set expectations, I think it's highly unlikely that we end up shipping a Ruby runtime with Zed.

It wouldn't be honest to say I completely agree, but I totally understand why shipping a Ruby runtime won't happen. I had a lot of fun working on this proof of concept, though.

I think a more promising direction would be exposing a process API to extensions to allow them to do this sort of thing themselves.

I have a start of one in #25399.

And I started a branch to play around with it in the Ruby extension: https://github.com/zed-extensions/ruby/blob/67d8485778f74a10909cd07644be27c5088dbfe9/src/language_servers/language_server.rs#L43-L47

Haven't wired up anything Ruby-specific yet, but I did confirm that the process execution is working.

That's awesome! Thank you! I can see what I can do with it and get back to you with the results if you'd like feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla-signed The user has signed the Contributor License Agreement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Ruby extension doesn't have a working language server by default
3 participants