ruby: Check if `solargraph` exists in `$PATH` or is configured (#10835)

Thorsten Ball created

This fixes #9811 by checking for the `solargraph` binary in the `$PATH`
as it's setup in the project shell.

It also adds support for configuring the path to `solargraph` manually:

```json
{
  "lsp": {
    "solargraph": {
      "binary": {
        "path": "/Users/thorstenball/bin/solargraph",
        "arguments": ["stdio"]
      }
    }
  }
}
```

## Example

Given the following setup:

- `ruby@3.3.0` used globally, no `solargraph` installed globally
- `ruby@3.2.2` used in a project, `solargraph` installed as binstub in
`$project/bin/solargraph`, `.envrc` to configure `direnv` to add
`$project/bin` to `$PATH

Which looks like this in practice:

```shell
# GLOBAL
~ $ ruby --version
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]
~ $ which solargraph
solargraph not found

# IN PROJECT
~ $ cd work/projs/rails-proj
direnv: loading ~/work/projs/rails-proj/.envrc
direnv: export ~PATH
~/work/projs/rails-proj $ ruby --version
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin23]
~/work/projs/rails-proj $ which solargraph
/Users/thorstenball/work/projs/rails-proj/bin/solargraph
```

The expectation is that Zed, when opening `~/work/projs/rails-proj`,
picks up the local `solargraph`.

But with **Zed Stable** that doesn't work, as we can see in the logs:

```
2024-04-22T10:21:37+02:00 [INFO] starting language server. binary path: "solargraph", working directory: "/Users/thorstenball/work/projs/rails-proj", args: ["stdio"]
2024-04-22T10:21:37+02:00 [ERROR] failed to start language server "solargraph": No such file or directory (os error 2)
```

With the change in this PR, it uses `rails/proj/bin/solargraph`:

```
[2024-04-22T10:33:06+02:00 INFO  language] found user-installed language server for Ruby. path: "/Users/thorstenball/work/projs/rails-proj/bin/solargraph", arguments: ["stdio"]
[2024-04-22T10:33:06+02:00 INFO  lsp] starting language server. binary path: "/Users/thorstenball/work/projs/rails-proj/bin/solargraph", working directory: "/Users/thorstenball/work/projs/rails-proj", args: ["stdio"]
```

**NOTE**: depending on whether `mise` (or `rbenv`, `asdf`, `chruby`,
...) or `direnv` come first in the shell-rc file, it picks one or the
other, depending on what puts itself first in `$PATH`.

## Release Notes

Release Notes:

- Added support for finding the Ruby language server `solargraph` in the
user's `$PATH` as it is when `cd`ing into a project's directory.
([#9811](https://github.com/zed-industries/zed/issues/9811))
- Added support for configuring the `path` and `arguments` for
`solargraph` language server manually. Example from settings: `{"lsp":
{"solargraph": {"binary":
{"path":"/Users/thorstenball/bin/solargraph","arguments": ["stdio"]}}}}`
([#9811](https://github.com/zed-industries/zed/issues/9811))

Change summary

crates/languages/src/ruby.rs | 54 +++++++++++++++++++++++++++++++++++--
1 file changed, 51 insertions(+), 3 deletions(-)

Detailed changes

crates/languages/src/ruby.rs 🔗

@@ -1,15 +1,63 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
+use gpui::AsyncAppContext;
 use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
-use std::{any::Any, path::PathBuf, sync::Arc};
+use project::project_settings::{BinarySettings, ProjectSettings};
+use settings::Settings;
+use std::{any::Any, ffi::OsString, path::PathBuf, sync::Arc};
 
 pub struct RubyLanguageServer;
 
+impl RubyLanguageServer {
+    const SERVER_NAME: &'static str = "solargraph";
+
+    fn server_binary_arguments() -> Vec<OsString> {
+        vec!["stdio".into()]
+    }
+}
+
 #[async_trait(?Send)]
 impl LspAdapter for RubyLanguageServer {
     fn name(&self) -> LanguageServerName {
-        LanguageServerName("solargraph".into())
+        LanguageServerName(Self::SERVER_NAME.into())
+    }
+
+    async fn check_if_user_installed(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+        cx: &AsyncAppContext,
+    ) -> Option<LanguageServerBinary> {
+        let configured_binary = cx.update(|cx| {
+            ProjectSettings::get_global(cx)
+                .lsp
+                .get(Self::SERVER_NAME)
+                .and_then(|s| s.binary.clone())
+        });
+
+        if let Ok(Some(BinarySettings {
+            path: Some(path),
+            arguments,
+        })) = configured_binary
+        {
+            Some(LanguageServerBinary {
+                path: path.into(),
+                arguments: arguments
+                    .unwrap_or_default()
+                    .iter()
+                    .map(|arg| arg.into())
+                    .collect(),
+                env: None,
+            })
+        } else {
+            let env = delegate.shell_env().await;
+            let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
+            Some(LanguageServerBinary {
+                path,
+                arguments: Self::server_binary_arguments(),
+                env: Some(env),
+            })
+        }
     }
 
     async fn fetch_latest_server_version(
@@ -36,7 +84,7 @@ impl LspAdapter for RubyLanguageServer {
         Some(LanguageServerBinary {
             path: "solargraph".into(),
             env: None,
-            arguments: vec!["stdio".into()],
+            arguments: Self::server_binary_arguments(),
         })
     }