ruby: Add support for "rubocop" language server (#14661)

Vitaly Slobodin and Marshall Bowers created

Hi, this pull request adds support for `rubocop` language server. I
noticed that `ruby-lsp` LS is becoming more popular but it still lacks
diagnostics support in Zed. To cover that missing feature, it could be
good to use `rubocop` LS to show diagnostics alongside with the running
Ruby LSP.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>

Change summary

assets/settings/default.json                    |  2 
docs/src/languages/ruby.md                      | 39 ++++++++++++++++++
extensions/ruby/extension.toml                  |  4 +
extensions/ruby/src/language_servers.rs         |  2 
extensions/ruby/src/language_servers/rubocop.rs | 19 +++++++++
extensions/ruby/src/ruby.rs                     | 13 +++++
6 files changed, 76 insertions(+), 3 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -798,7 +798,7 @@
       }
     },
     "Ruby": {
-      "language_servers": ["solargraph", "!ruby-lsp", "..."]
+      "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "..."]
     },
     "SCSS": {
       "prettier": {

docs/src/languages/ruby.md 🔗

@@ -16,7 +16,20 @@ To switch to `ruby-lsp`, add the following to your `settings.json`:
 {
   "languages": {
     "Ruby": {
-      "language_servers": ["ruby-lsp", "!solargraph", "..."]
+      "language_servers": ["ruby-lsp", "!solargraph", "!rubocop", "..."]
+    }
+  }
+}
+```
+
+The Ruby extension also provides support for `rubocop` language server for offense detection and autocorrection. To enable it, add the following to your
+`settings.json`:
+
+```json
+{
+  "languages": {
+    "Ruby": {
+      "language_servers": ["rubocop", "ruby-lsp", "!solargraph", "..."]
     }
   }
 }
@@ -83,6 +96,30 @@ Ruby LSP uses pull-based diagnostics which Zed doesn't support yet. We can tell
 }
 ```
 
+## Setting up `rubocop` LSP
+
+Zed currently doesn't install `rubocop` automatically. To use `rubocop`, you need to install the gem. Zed just looks for an executable called `rubocop` on your `PATH`.
+
+You can install the gem manually with the following command:
+
+```shell
+gem install rubocop
+```
+
+Rubocop has unsafe autocorrection disabled by default. We can tell Zed to enable it by adding the following to your `settings.json`:
+
+```json
+{
+  "lsp": {
+    "rubocop": {
+      "initialization_options": {
+        "safeAutocorrect": false
+      }
+    }
+  }
+}
+```
+
 ## 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.

extensions/ruby/extension.toml 🔗

@@ -14,6 +14,10 @@ language = "Ruby"
 name = "Ruby LSP"
 language = "Ruby"
 
+[language_servers.rubocop]
+name = "Rubocop"
+language = "Ruby"
+
 [grammars.ruby]
 repository = "https://github.com/tree-sitter/tree-sitter-ruby"
 commit = "dc2d7d6b50f9975bc3c35bbec0ba11b2617b736b"

extensions/ruby/src/language_servers/rubocop.rs 🔗

@@ -0,0 +1,19 @@
+use zed_extension_api::{self as zed, Result};
+
+pub struct Rubocop {}
+
+impl Rubocop {
+    pub const LANGUAGE_SERVER_ID: &'static str = "rubocop";
+
+    pub fn new() -> Self {
+        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()
+        })?;
+
+        Ok(path)
+    }
+}

extensions/ruby/src/ruby.rs 🔗

@@ -5,11 +5,12 @@ use zed::settings::LspSettings;
 use zed::{serde_json, CodeLabel, LanguageServerId};
 use zed_extension_api::{self as zed, Result};
 
-use crate::language_servers::{RubyLsp, Solargraph};
+use crate::language_servers::{Rubocop, RubyLsp, Solargraph};
 
 struct RubyExtension {
     solargraph: Option<Solargraph>,
     ruby_lsp: Option<RubyLsp>,
+    rubocop: Option<Rubocop>,
 }
 
 impl zed::Extension for RubyExtension {
@@ -17,6 +18,7 @@ impl zed::Extension for RubyExtension {
         Self {
             solargraph: None,
             ruby_lsp: None,
+            rubocop: None,
         }
     }
 
@@ -44,6 +46,15 @@ impl zed::Extension for RubyExtension {
                     env: worktree.shell_env(),
                 })
             }
+            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(),
+                })
+            }
             language_server_id => Err(format!("unknown language server: {language_server_id}")),
         }
     }