ruby: Support "binary" settings for Rubocop and Solargraph (#15110)

Hello, this pull request adds support for specifying and using the
"binary" settings for Rubocop and Solargraph LSPs. AFAIK, Ruby LSP does
not require the bundler context but that could be added later easily.

In Ruby world, like in Node.js world, almost all
projects rely on project specific packages (gems) and their versions.
Solargraph and Rubocop gems are usually installed as project
dependencies. Attempting to use global installation of them fail in most
cases due to incompatible or missing dependencies (gems).

To avoid that, Ruby engineers have the `bundler`
gem that provides the `exec` command. This command executes the given
command in the context of the bundle.

This pull request adds support for pulling the `binary` settings to use
them in starting both LSPs. For instance, to start the Solargraph gem in
the context of the bundler, the end user must configure the binary
settings in the folder-specific settings file like so:

```json
{
  "lsp": {
    "solargraph": {
      "binary": {
        "path": "/Users/vslobodin/Development/festivatica/bin/rubocop"
      }
    }
  }
}
```

The `path` key must be an absolute path to the `binstub` of the
`solargraph` gem. The same applies to the "rubocop" gem. Side note but
it would be awesome to use Zed specific environment variables to make
this a bit easier. For instance, we could use the `ZED_WORKTREE_ROOT`
environment variable:

```json
{
  "lsp": {
    "solargraph": {
      "binary": {
        "path": "${ZED_WORKTREE_ROOT}/bin/rubocop"
      }
    }
  }
}
```

But this is out of the scope of this pull request. The code is a bit
messy and repeatable in some places, I am happy to improve it here or
later.

References:
- https://bundler.io/v2.4/man/bundle-exec.1.html
- https://solargraph.org/guides/troubleshooting
- https://bundler.io/v2.5/man/bundle-binstubs.1.html

This pull request is based on these two pull requests:

- https://github.com/zed-industries/zed/pull/14655
- https://github.com/zed-industries/zed/issues/15001

Closes https://github.com/zed-industries/zed/issues/5109.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
Vitaly Slobodin 2024-08-06 14:36:14 +02:00 committed by GitHub
parent 7cef5b2956
commit 1c3f303594
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 176 additions and 39 deletions

View File

@ -66,6 +66,20 @@ Solargraph has formatting and diagnostics disabled by default. We can tell Zed t
}
```
To use Solargraph in the context of the bundle, you can use [folder-specific settings](../configuring-zed#settings-files) and specify the absolute path to the [`binstub`](https://bundler.io/v2.5/man/bundle-binstubs.1.html) of Solargraph:
```json
{
"lsp": {
"solargraph": {
"binary": {
"path": "<path_to_your_project>/bin/solargraph"
}
}
}
}
```
### Configuration
Solargraph reads its configuration from a file called `.solargraph.yml` in the root of your project. For more information about this file, see the [Solargraph configuration documentation](https://solargraph.org/guides/configuration).
@ -120,6 +134,20 @@ Rubocop has unsafe autocorrection disabled by default. We can tell Zed to enable
}
```
To use Rubocop in the context of the bundle, you can use [folder-specific settings](../configuring-zed#settings-files) and specify the absolute path to the [`binstub`](https://bundler.io/v2.5/man/bundle-binstubs.1.html) of Rubocop:
```json
{
"lsp": {
"rubocop": {
"binary": {
"path": "<path_to_your_project>/bin/rubocop"
}
}
}
}
```
## Using the Tailwind CSS Language Server with Ruby
It's possible to use the [Tailwind CSS Language Server](https://github.com/tailwindlabs/tailwindcss-intellisense/tree/HEAD/packages/tailwindcss-language-server#readme) in Ruby and ERB files.

View File

@ -1,4 +1,9 @@
use zed_extension_api::{self as zed, Result};
use zed_extension_api::{self as zed, settings::LspSettings, LanguageServerId, Result};
pub struct RubocopBinary {
pub path: String,
pub args: Option<Vec<String>>,
}
pub struct Rubocop {}
@ -9,11 +14,46 @@ impl Rubocop {
Self {}
}
pub fn server_script_path(&mut self, worktree: &zed::Worktree) -> Result<String> {
let path = worktree.which("rubocop").ok_or_else(|| {
"rubocop must be installed manually. Install it with `gem install rubocop` or specify the 'binary' path to it via local settings.".to_string()
})?;
pub fn language_server_command(
&mut self,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<zed::Command> {
let binary = self.language_server_binary(language_server_id, worktree)?;
Ok(path)
Ok(zed::Command {
command: binary.path,
args: binary.args.unwrap_or_else(|| vec!["--lsp".to_string()]),
env: worktree.shell_env(),
})
}
fn language_server_binary(
&self,
_language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<RubocopBinary> {
let binary_settings = LspSettings::for_worktree("rubocop", worktree)
.ok()
.and_then(|lsp_settings| lsp_settings.binary);
let binary_args = binary_settings
.as_ref()
.and_then(|binary_settings| binary_settings.arguments.clone());
if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) {
return Ok(RubocopBinary {
path,
args: binary_args,
});
}
if let Some(path) = worktree.which("rubocop") {
return Ok(RubocopBinary {
path,
args: binary_args,
});
}
Err("rubocop must be installed manually. Install it with `gem install rubocop` or specify the 'binary' path to it via local settings.".to_string())
}
}

View File

@ -1,8 +1,14 @@
use zed::{
use zed_extension_api::{
self as zed,
lsp::{Completion, CompletionKind, Symbol, SymbolKind},
CodeLabel, CodeLabelSpan,
settings::LspSettings,
CodeLabel, CodeLabelSpan, LanguageServerId, Result,
};
use zed_extension_api::{self as zed, Result};
pub struct RubyLspBinary {
pub path: String,
pub args: Option<Vec<String>>,
}
pub struct RubyLsp {}
@ -13,13 +19,50 @@ impl RubyLsp {
Self {}
}
pub fn server_script_path(&mut self, worktree: &zed::Worktree) -> Result<String> {
let path = worktree.which("ruby-lsp").ok_or_else(|| {
"ruby-lsp must be installed manually. Install it with `gem install ruby-lsp`."
.to_string()
})?;
pub fn language_server_command(
&mut self,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<zed::Command> {
let binary = self.language_server_binary(language_server_id, worktree)?;
Ok(path)
Ok(zed::Command {
command: binary.path,
args: binary.args.unwrap_or_default(),
env: worktree.shell_env(),
})
}
fn language_server_binary(
&self,
_language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<RubyLspBinary> {
let binary_settings = LspSettings::for_worktree("ruby-lsp", worktree)
.ok()
.and_then(|lsp_settings| lsp_settings.binary);
let binary_args = binary_settings
.as_ref()
.and_then(|binary_settings| binary_settings.arguments.clone());
if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) {
return Ok(RubyLspBinary {
path,
args: binary_args,
});
}
if let Some(path) = worktree.which("ruby-lsp") {
return Ok(RubyLspBinary {
path,
args: binary_args,
});
}
Err(
"ruby-lsp must be installed manually. Install it with `gem install ruby-lsp`."
.to_string(),
)
}
pub fn label_for_completion(&self, completion: Completion) -> Option<CodeLabel> {

View File

@ -1,6 +1,12 @@
use zed::lsp::{Completion, CompletionKind, Symbol, SymbolKind};
use zed::{CodeLabel, CodeLabelSpan};
use zed_extension_api::{self as zed, Result};
use zed_extension_api::settings::LspSettings;
use zed_extension_api::{self as zed, LanguageServerId, Result};
pub struct SolargraphBinary {
pub path: String,
pub args: Option<Vec<String>>,
}
pub struct Solargraph {}
@ -11,12 +17,47 @@ impl Solargraph {
Self {}
}
pub fn server_script_path(&mut self, worktree: &zed::Worktree) -> Result<String> {
let path = worktree
.which("solargraph")
.ok_or_else(|| "solargraph must be installed manually".to_string())?;
pub fn language_server_command(
&mut self,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<zed::Command> {
let binary = self.language_server_binary(language_server_id, worktree)?;
Ok(path)
Ok(zed::Command {
command: binary.path,
args: binary.args.unwrap_or_else(|| vec!["stdio".to_string()]),
env: worktree.shell_env(),
})
}
fn language_server_binary(
&self,
_language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<SolargraphBinary> {
let binary_settings = LspSettings::for_worktree("solargraph", worktree)
.ok()
.and_then(|lsp_settings| lsp_settings.binary);
let binary_args = binary_settings
.as_ref()
.and_then(|binary_settings| binary_settings.arguments.clone());
if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) {
return Ok(SolargraphBinary {
path,
args: binary_args,
});
}
if let Some(path) = worktree.which("solargraph") {
return Ok(SolargraphBinary {
path,
args: binary_args,
});
}
Err("solargraph must be installed manually".to_string())
}
pub fn label_for_completion(&self, completion: Completion) -> Option<CodeLabel> {

View File

@ -30,30 +30,15 @@ impl zed::Extension for RubyExtension {
match language_server_id.as_ref() {
Solargraph::LANGUAGE_SERVER_ID => {
let solargraph = self.solargraph.get_or_insert_with(|| Solargraph::new());
Ok(zed::Command {
command: solargraph.server_script_path(worktree)?,
args: vec!["stdio".into()],
env: worktree.shell_env(),
})
solargraph.language_server_command(language_server_id, worktree)
}
RubyLsp::LANGUAGE_SERVER_ID => {
let ruby_lsp = self.ruby_lsp.get_or_insert_with(|| RubyLsp::new());
Ok(zed::Command {
command: ruby_lsp.server_script_path(worktree)?,
args: vec![],
env: worktree.shell_env(),
})
ruby_lsp.language_server_command(language_server_id, worktree)
}
Rubocop::LANGUAGE_SERVER_ID => {
let rubocop = self.rubocop.get_or_insert_with(|| Rubocop::new());
Ok(zed::Command {
command: rubocop.server_script_path(worktree)?,
args: vec!["--lsp".into()],
env: worktree.shell_env(),
})
rubocop.language_server_command(language_server_id, worktree)
}
language_server_id => Err(format!("unknown language server: {language_server_id}")),
}