From 1c3f30359488d7f74ceb0a3c83ba28b24f3edde9 Mon Sep 17 00:00:00 2001 From: Vitaly Slobodin Date: Tue, 6 Aug 2024 14:36:14 +0200 Subject: [PATCH] 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 --- docs/src/languages/ruby.md | 28 +++++++++ .../ruby/src/language_servers/rubocop.rs | 52 ++++++++++++++-- .../ruby/src/language_servers/ruby_lsp.rs | 61 ++++++++++++++++--- .../ruby/src/language_servers/solargraph.rs | 53 ++++++++++++++-- extensions/ruby/src/ruby.rs | 21 +------ 5 files changed, 176 insertions(+), 39 deletions(-) diff --git a/docs/src/languages/ruby.md b/docs/src/languages/ruby.md index 331a9797abbc615a02ea61824b2b35be08ff3802..19979bea9712c211c7e85343f815a501fd27d1fd 100644 --- a/docs/src/languages/ruby.md +++ b/docs/src/languages/ruby.md @@ -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": "/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": "/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. diff --git a/extensions/ruby/src/language_servers/rubocop.rs b/extensions/ruby/src/language_servers/rubocop.rs index 6ce2621665f12d3d7db186856d34af9a11bfb190..d8e342bd510199452fc6c02e051a2e09903d11e2 100644 --- a/extensions/ruby/src/language_servers/rubocop.rs +++ b/extensions/ruby/src/language_servers/rubocop.rs @@ -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>, +} pub struct Rubocop {} @@ -9,11 +14,46 @@ impl Rubocop { Self {} } - pub fn server_script_path(&mut self, worktree: &zed::Worktree) -> Result { - 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 { + let binary = self.language_server_binary(language_server_id, worktree)?; + + 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 { + 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, + }); + } - Ok(path) + Err("rubocop must be installed manually. Install it with `gem install rubocop` or specify the 'binary' path to it via local settings.".to_string()) } } diff --git a/extensions/ruby/src/language_servers/ruby_lsp.rs b/extensions/ruby/src/language_servers/ruby_lsp.rs index fcac42328443c77554b1231beae19d5b7463012e..52c5a67ffbdc3aaf08ae12f14c811af64f7ea5dd 100644 --- a/extensions/ruby/src/language_servers/ruby_lsp.rs +++ b/extensions/ruby/src/language_servers/ruby_lsp.rs @@ -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>, +} pub struct RubyLsp {} @@ -13,13 +19,50 @@ impl RubyLsp { Self {} } - pub fn server_script_path(&mut self, worktree: &zed::Worktree) -> Result { - 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 { + let binary = self.language_server_binary(language_server_id, worktree)?; + + 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 { + 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, + }); + } - Ok(path) + 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 { diff --git a/extensions/ruby/src/language_servers/solargraph.rs b/extensions/ruby/src/language_servers/solargraph.rs index 32e9f6b173acd47232f2028120f4e82bfe6dfdea..af736d610466b28824211022342f17d47890c9ca 100644 --- a/extensions/ruby/src/language_servers/solargraph.rs +++ b/extensions/ruby/src/language_servers/solargraph.rs @@ -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>, +} pub struct Solargraph {} @@ -11,12 +17,47 @@ impl Solargraph { Self {} } - pub fn server_script_path(&mut self, worktree: &zed::Worktree) -> Result { - 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 { + let binary = self.language_server_binary(language_server_id, worktree)?; + + Ok(zed::Command { + command: binary.path, + args: binary.args.unwrap_or_else(|| vec!["stdio".to_string()]), + env: worktree.shell_env(), + }) + } - Ok(path) + fn language_server_binary( + &self, + _language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + 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 { diff --git a/extensions/ruby/src/ruby.rs b/extensions/ruby/src/ruby.rs index af49202fdffc26982602226054b821109acfe42d..e9038d52be6656f7f5bb0ac8a24de4513024791e 100644 --- a/extensions/ruby/src/ruby.rs +++ b/extensions/ruby/src/ruby.rs @@ -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}")), }