Files
oak-gpui/crates/languages/src/ruby.rs
T
Thorsten Ball a0fa8a489b ruby: Check if solargraph exists in $PATH or is configured (#10835)
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))
2024-04-22 10:44:05 +02:00

206 lines
6.5 KiB
Rust

use anyhow::{anyhow, Result};
use async_trait::async_trait;
use gpui::AsyncAppContext;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
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(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(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(()))
}
async fn fetch_server_binary(
&self,
_version: Box<dyn 'static + Send + Any>,
_container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
Err(anyhow!("solargraph must be installed manually"))
}
async fn cached_server_binary(
&self,
_: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
Some(LanguageServerBinary {
path: "solargraph".into(),
env: None,
arguments: Self::server_binary_arguments(),
})
}
fn can_be_reinstalled(&self) -> bool {
false
}
async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
None
}
async fn label_for_completion(
&self,
item: &lsp::CompletionItem,
language: &Arc<language::Language>,
) -> Option<language::CodeLabel> {
let label = &item.label;
let grammar = language.grammar()?;
let highlight_id = match item.kind? {
lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
lsp::CompletionItemKind::CLASS | lsp::CompletionItemKind::MODULE => {
grammar.highlight_id_for_name("type")?
}
lsp::CompletionItemKind::KEYWORD => {
if label.starts_with(':') {
grammar.highlight_id_for_name("string.special.symbol")?
} else {
grammar.highlight_id_for_name("keyword")?
}
}
lsp::CompletionItemKind::VARIABLE => {
if label.starts_with('@') {
grammar.highlight_id_for_name("property")?
} else {
return None;
}
}
_ => return None,
};
Some(language::CodeLabel {
text: label.clone(),
runs: vec![(0..label.len(), highlight_id)],
filter_range: 0..label.len(),
})
}
async fn label_for_symbol(
&self,
label: &str,
kind: lsp::SymbolKind,
language: &Arc<language::Language>,
) -> Option<language::CodeLabel> {
let grammar = language.grammar()?;
match kind {
lsp::SymbolKind::METHOD => {
let mut parts = label.split('#');
let classes = parts.next()?;
let method = parts.next()?;
if parts.next().is_some() {
return None;
}
let class_id = grammar.highlight_id_for_name("type")?;
let method_id = grammar.highlight_id_for_name("function.method")?;
let mut ix = 0;
let mut runs = Vec::new();
for (i, class) in classes.split("::").enumerate() {
if i > 0 {
ix += 2;
}
let end_ix = ix + class.len();
runs.push((ix..end_ix, class_id));
ix = end_ix;
}
ix += 1;
let end_ix = ix + method.len();
runs.push((ix..end_ix, method_id));
Some(language::CodeLabel {
text: label.to_string(),
runs,
filter_range: 0..label.len(),
})
}
lsp::SymbolKind::CONSTANT => {
let constant_id = grammar.highlight_id_for_name("constant")?;
Some(language::CodeLabel {
text: label.to_string(),
runs: vec![(0..label.len(), constant_id)],
filter_range: 0..label.len(),
})
}
lsp::SymbolKind::CLASS | lsp::SymbolKind::MODULE => {
let class_id = grammar.highlight_id_for_name("type")?;
let mut ix = 0;
let mut runs = Vec::new();
for (i, class) in label.split("::").enumerate() {
if i > 0 {
ix += "::".len();
}
let end_ix = ix + class.len();
runs.push((ix..end_ix, class_id));
ix = end_ix;
}
Some(language::CodeLabel {
text: label.to_string(),
runs,
filter_range: 0..label.len(),
})
}
_ => return None,
}
}
}