Add ruby LSP support via SolarGraph

Max Brunsfeld created

Change summary

crates/zed/src/languages.rs      |  13 ++
crates/zed/src/languages/ruby.rs | 145 ++++++++++++++++++++++++++++++++++
2 files changed, 156 insertions(+), 2 deletions(-)

Detailed changes

crates/zed/src/languages.rs 🔗

@@ -12,6 +12,7 @@ mod installation;
 mod json;
 mod language_plugin;
 mod python;
+mod ruby;
 mod rust;
 mod typescript;
 
@@ -116,8 +117,16 @@ pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>)
             tree_sitter_html::language(),
             Some(CachedLspAdapter::new(html::HtmlLspAdapter).await),
         ),
-        ("ruby", tree_sitter_ruby::language(), None),
-        ("erb", tree_sitter_embedded_template::language(), None),
+        (
+            "ruby",
+            tree_sitter_ruby::language(),
+            Some(CachedLspAdapter::new(ruby::RubyLanguageServer).await),
+        ),
+        (
+            "erb",
+            tree_sitter_embedded_template::language(),
+            Some(CachedLspAdapter::new(ruby::RubyLanguageServer).await),
+        ),
     ] {
         languages.add(language(name, grammar, lsp_adapter));
     }

crates/zed/src/languages/ruby.rs 🔗

@@ -0,0 +1,145 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use client::http::HttpClient;
+use language::{LanguageServerName, LspAdapter};
+use std::{any::Any, path::PathBuf, sync::Arc};
+
+pub struct RubyLanguageServer;
+
+#[async_trait]
+impl LspAdapter for RubyLanguageServer {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("solargraph".into())
+    }
+
+    async fn server_args(&self) -> Vec<String> {
+        vec!["stdio".into()]
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: Arc<dyn HttpClient>,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(()))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _version: Box<dyn 'static + Send + Any>,
+        _: Arc<dyn HttpClient>,
+        _container_dir: PathBuf,
+    ) -> Result<PathBuf> {
+        Err(anyhow!("solargraph must be installed manually"))
+    }
+
+    async fn cached_server_binary(&self, _container_dir: PathBuf) -> Option<PathBuf> {
+        Some("solargraph".into())
+    }
+
+    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,
+        }
+    }
+}