From 400e938997029e2c4fa943a43a7a3b3af1134b78 Mon Sep 17 00:00:00 2001 From: Vitaly Slobodin Date: Fri, 10 May 2024 17:53:11 +0200 Subject: [PATCH] Extract Ruby extension (#11360) This PR extracts Ruby and ERB support into an extension and removes the built-in Ruby and Ruby support from Zed. As part of this, the new extension is prepared for adding support for the `Ruby LSP` which has some blockers. See https://github.com/zed-industries/zed/pull/8613 I was thinking of adding an initial support for Ruby LSP but I think it would be better to start with extracting the Ruby extension for now. The implementation, as the 1st step, matches the bundled version but with 3 differences: 1. Added signature output to the completion popup. See my comment below. ![CleanShot 2024-05-04 at 09 17 37@2x](https://github.com/zed-industries/zed/assets/1894248/486b7a48-ea0c-44ce-b0c9-9f8f5d3ad42d) 3. Use the shell environment for starting the `solargraph` executable. See my comment below. 4. Bumped the tree sitter version for Ruby to the latest available version. Additionally, I plan to tweak this extension a bit in the future but I think we should do this bit by bit. Thanks! Release Notes: - Removed built-in support for Ruby, in favor of making it available as an extension. --------- Co-authored-by: Marshall Bowers --- Cargo.lock | 8 +- Cargo.toml | 1 + assets/settings/default.json | 3 + crates/extensions_ui/src/extension_suggest.rs | 1 + crates/languages/Cargo.toml | 1 - crates/languages/src/lib.rs | 4 - crates/languages/src/ruby.rs | 205 ------------------ extensions/ruby/Cargo.toml | 16 ++ extensions/ruby/LICENSE-APACHE | 1 + extensions/ruby/extension.toml | 15 ++ .../ruby/languages}/erb/config.toml | 0 .../ruby/languages}/erb/highlights.scm | 0 .../ruby/languages}/erb/injections.scm | 0 .../ruby/languages}/ruby/brackets.scm | 0 .../ruby/languages}/ruby/config.toml | 0 .../ruby/languages}/ruby/embedding.scm | 0 .../ruby/languages}/ruby/highlights.scm | 0 .../ruby/languages}/ruby/indents.scm | 0 .../ruby/languages}/ruby/outline.scm | 0 .../ruby/languages}/ruby/overrides.scm | 0 extensions/ruby/src/language_servers.rs | 3 + .../ruby/src/language_servers/solargraph.rs | 121 +++++++++++ extensions/ruby/src/ruby.rs | 62 ++++++ 23 files changed, 230 insertions(+), 211 deletions(-) delete mode 100644 crates/languages/src/ruby.rs create mode 100644 extensions/ruby/Cargo.toml create mode 120000 extensions/ruby/LICENSE-APACHE create mode 100644 extensions/ruby/extension.toml rename {crates/languages/src => extensions/ruby/languages}/erb/config.toml (100%) rename {crates/languages/src => extensions/ruby/languages}/erb/highlights.scm (100%) rename {crates/languages/src => extensions/ruby/languages}/erb/injections.scm (100%) rename {crates/languages/src => extensions/ruby/languages}/ruby/brackets.scm (100%) rename {crates/languages/src => extensions/ruby/languages}/ruby/config.toml (100%) rename {crates/languages/src => extensions/ruby/languages}/ruby/embedding.scm (100%) rename {crates/languages/src => extensions/ruby/languages}/ruby/highlights.scm (100%) rename {crates/languages/src => extensions/ruby/languages}/ruby/indents.scm (100%) rename {crates/languages/src => extensions/ruby/languages}/ruby/outline.scm (100%) rename {crates/languages/src => extensions/ruby/languages}/ruby/overrides.scm (100%) create mode 100644 extensions/ruby/src/language_servers.rs create mode 100644 extensions/ruby/src/language_servers/solargraph.rs create mode 100644 extensions/ruby/src/ruby.rs diff --git a/Cargo.lock b/Cargo.lock index 53dbb11a59..2105303f54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5720,7 +5720,6 @@ dependencies = [ "tree-sitter-proto", "tree-sitter-python", "tree-sitter-regex", - "tree-sitter-ruby", "tree-sitter-rust", "tree-sitter-typescript", "tree-sitter-yaml", @@ -13114,6 +13113,13 @@ dependencies = [ "zed_extension_api 0.0.4", ] +[[package]] +name = "zed_ruby" +version = "0.0.1" +dependencies = [ + "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "zed_svelte" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 5785e5b7ab..fc8ff82ac1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,6 +127,7 @@ members = [ "extensions/php", "extensions/prisma", "extensions/purescript", + "extensions/ruby", "extensions/svelte", "extensions/terraform", "extensions/toml", diff --git a/assets/settings/default.json b/assets/settings/default.json index f656d3ad90..f78b82fd0d 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -633,6 +633,9 @@ }, "Prisma": { "tab_size": 2 + }, + "Ruby": { + "language_servers": ["solargraph", "..."] } }, // Zed's Prettier integration settings. diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs index db8e179c3e..ff869003e5 100644 --- a/crates/extensions_ui/src/extension_suggest.rs +++ b/crates/extensions_ui/src/extension_suggest.rs @@ -58,6 +58,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[ ("r", &["r", "R"]), ("racket", &["rkt"]), ("rescript", &["res", "resi"]), + ("ruby", &["rb", "erb"]), ("scheme", &["scm"]), ("scss", &["scss"]), ("sql", &["sql"]), diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 788ced8362..e13cd21c19 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -46,7 +46,6 @@ tree-sitter-markdown.workspace = true tree-sitter-proto.workspace = true tree-sitter-python.workspace = true tree-sitter-regex.workspace = true -tree-sitter-ruby.workspace = true tree-sitter-rust.workspace = true tree-sitter-typescript.workspace = true tree-sitter-yaml.workspace = true diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 3ce3073f3a..5d4f0d8ad2 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -16,7 +16,6 @@ mod css; mod go; mod json; mod python; -mod ruby; mod rust; mod tailwind; mod typescript; @@ -50,7 +49,6 @@ pub fn init( ("proto", tree_sitter_proto::language()), ("python", tree_sitter_python::language()), ("regex", tree_sitter_regex::language()), - ("ruby", tree_sitter_ruby::language()), ("rust", tree_sitter_rust::language()), ("tsx", tree_sitter_typescript::language_tsx()), ("typescript", tree_sitter_typescript::language_typescript()), @@ -156,8 +154,6 @@ pub fn init( node_runtime.clone(), ))] ); - language!("ruby", vec![Arc::new(ruby::RubyLanguageServer)]); - language!("erb", vec![Arc::new(ruby::RubyLanguageServer),]); language!("regex"); language!( "yaml", diff --git a/crates/languages/src/ruby.rs b/crates/languages/src/ruby.rs deleted file mode 100644 index f7c32f8f85..0000000000 --- a/crates/languages/src/ruby.rs +++ /dev/null @@ -1,205 +0,0 @@ -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 { - 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 { - 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> { - Ok(Box::new(())) - } - - async fn fetch_server_binary( - &self, - _version: Box, - _container_dir: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Result { - Err(anyhow!("solargraph must be installed manually")) - } - - async fn cached_server_binary( - &self, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - 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 { - None - } - - async fn label_for_completion( - &self, - item: &lsp::CompletionItem, - language: &Arc, - ) -> Option { - 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, - ) -> Option { - 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, - } - } -} diff --git a/extensions/ruby/Cargo.toml b/extensions/ruby/Cargo.toml new file mode 100644 index 0000000000..0f335ef40c --- /dev/null +++ b/extensions/ruby/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "zed_ruby" +version = "0.0.1" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/ruby.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = "0.0.6" diff --git a/extensions/ruby/LICENSE-APACHE b/extensions/ruby/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/extensions/ruby/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/ruby/extension.toml b/extensions/ruby/extension.toml new file mode 100644 index 0000000000..4fbfa3aaaa --- /dev/null +++ b/extensions/ruby/extension.toml @@ -0,0 +1,15 @@ +id = "ruby" +name = "Ruby" +description = "Ruby support." +version = "0.0.1" +schema_version = 1 +authors = ["Vitaly Slobodin "] +repository = "https://github.com/zed-industries/zed" + +[language_servers.solargraph] +name = "Solargraph" +language = "Ruby" + +[grammars.ruby] +repository = "https://github.com/tree-sitter/tree-sitter-ruby" +commit = "9d86f3761bb30e8dcc81e754b81d3ce91848477e" diff --git a/crates/languages/src/erb/config.toml b/extensions/ruby/languages/erb/config.toml similarity index 100% rename from crates/languages/src/erb/config.toml rename to extensions/ruby/languages/erb/config.toml diff --git a/crates/languages/src/erb/highlights.scm b/extensions/ruby/languages/erb/highlights.scm similarity index 100% rename from crates/languages/src/erb/highlights.scm rename to extensions/ruby/languages/erb/highlights.scm diff --git a/crates/languages/src/erb/injections.scm b/extensions/ruby/languages/erb/injections.scm similarity index 100% rename from crates/languages/src/erb/injections.scm rename to extensions/ruby/languages/erb/injections.scm diff --git a/crates/languages/src/ruby/brackets.scm b/extensions/ruby/languages/ruby/brackets.scm similarity index 100% rename from crates/languages/src/ruby/brackets.scm rename to extensions/ruby/languages/ruby/brackets.scm diff --git a/crates/languages/src/ruby/config.toml b/extensions/ruby/languages/ruby/config.toml similarity index 100% rename from crates/languages/src/ruby/config.toml rename to extensions/ruby/languages/ruby/config.toml diff --git a/crates/languages/src/ruby/embedding.scm b/extensions/ruby/languages/ruby/embedding.scm similarity index 100% rename from crates/languages/src/ruby/embedding.scm rename to extensions/ruby/languages/ruby/embedding.scm diff --git a/crates/languages/src/ruby/highlights.scm b/extensions/ruby/languages/ruby/highlights.scm similarity index 100% rename from crates/languages/src/ruby/highlights.scm rename to extensions/ruby/languages/ruby/highlights.scm diff --git a/crates/languages/src/ruby/indents.scm b/extensions/ruby/languages/ruby/indents.scm similarity index 100% rename from crates/languages/src/ruby/indents.scm rename to extensions/ruby/languages/ruby/indents.scm diff --git a/crates/languages/src/ruby/outline.scm b/extensions/ruby/languages/ruby/outline.scm similarity index 100% rename from crates/languages/src/ruby/outline.scm rename to extensions/ruby/languages/ruby/outline.scm diff --git a/crates/languages/src/ruby/overrides.scm b/extensions/ruby/languages/ruby/overrides.scm similarity index 100% rename from crates/languages/src/ruby/overrides.scm rename to extensions/ruby/languages/ruby/overrides.scm diff --git a/extensions/ruby/src/language_servers.rs b/extensions/ruby/src/language_servers.rs new file mode 100644 index 0000000000..bd6252d586 --- /dev/null +++ b/extensions/ruby/src/language_servers.rs @@ -0,0 +1,3 @@ +mod solargraph; + +pub use solargraph::*; diff --git a/extensions/ruby/src/language_servers/solargraph.rs b/extensions/ruby/src/language_servers/solargraph.rs new file mode 100644 index 0000000000..a1207b70b2 --- /dev/null +++ b/extensions/ruby/src/language_servers/solargraph.rs @@ -0,0 +1,121 @@ +use zed::lsp::{Completion, CompletionKind, Symbol, SymbolKind}; +use zed::{CodeLabel, CodeLabelSpan}; +use zed_extension_api::{self as zed, Result}; + +pub struct Solargraph {} + +impl Solargraph { + pub const LANGUAGE_SERVER_ID: &'static str = "solargraph"; + + pub fn new() -> Self { + 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())?; + + Ok(path) + } + + pub fn label_for_completion(&self, completion: Completion) -> Option { + match completion.kind? { + CompletionKind::Method => { + let highlight_name = match completion.kind? { + CompletionKind::Class | CompletionKind::Module => "type", + CompletionKind::Constant => "constant", + CompletionKind::Method => "function.method", + CompletionKind::Keyword => { + if completion.label.starts_with(':') { + "string.special.symbol" + } else { + "keyword" + } + } + CompletionKind::Variable => { + if completion.label.starts_with('@') { + "property" + } else { + return None; + } + } + _ => return None, + }; + + let len = completion.label.len(); + let name_span = + CodeLabelSpan::literal(completion.label, Some(highlight_name.to_string())); + + Some(CodeLabel { + code: Default::default(), + spans: if let Some(detail) = completion.detail { + vec![ + name_span, + CodeLabelSpan::literal(" ", None), + CodeLabelSpan::literal(detail, None), + ] + } else { + vec![name_span] + }, + filter_range: (0..len).into(), + }) + } + _ => None, + } + } + + pub fn label_for_symbol(&self, symbol: Symbol) -> Option { + let name = &symbol.name; + + return match symbol.kind { + SymbolKind::Method => { + let mut parts = name.split('#'); + let container_name = parts.next()?; + let method_name = parts.next()?; + + if parts.next().is_some() { + return None; + } + + let filter_range = 0..name.len(); + + let spans = vec![ + CodeLabelSpan::literal(container_name, Some("type".to_string())), + CodeLabelSpan::literal("#", None), + CodeLabelSpan::literal(method_name, Some("function.method".to_string())), + ]; + + Some(CodeLabel { + code: name.to_string(), + spans, + filter_range: filter_range.into(), + }) + } + SymbolKind::Class | SymbolKind::Module => { + let class = "class "; + let code = format!("{class}{name}"); + let filter_range = 0..name.len(); + let display_range = class.len()..class.len() + name.len(); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range(display_range)], + filter_range: filter_range.into(), + }) + } + SymbolKind::Constant => { + let code = name.to_uppercase().to_string(); + let filter_range = 0..name.len(); + let display_range = 0..name.len(); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range(display_range)], + filter_range: filter_range.into(), + }) + } + _ => None, + }; + } +} diff --git a/extensions/ruby/src/ruby.rs b/extensions/ruby/src/ruby.rs new file mode 100644 index 0000000000..18795ba295 --- /dev/null +++ b/extensions/ruby/src/ruby.rs @@ -0,0 +1,62 @@ +mod language_servers; + +use zed::lsp::{Completion, Symbol}; +use zed::{CodeLabel, LanguageServerId}; +use zed_extension_api::{self as zed, Result}; + +use crate::language_servers::Solargraph; + +struct RubyExtension { + solargraph: Option, +} + +impl zed::Extension for RubyExtension { + fn new() -> Self { + Self { solargraph: None } + } + + fn language_server_command( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + 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(), + }) + } + language_server_id => Err(format!("unknown language server: {language_server_id}")), + } + } + + fn label_for_symbol( + &self, + language_server_id: &LanguageServerId, + symbol: Symbol, + ) -> Option { + match language_server_id.as_ref() { + Solargraph::LANGUAGE_SERVER_ID => self.solargraph.as_ref()?.label_for_symbol(symbol), + _ => None, + } + } + + fn label_for_completion( + &self, + language_server_id: &LanguageServerId, + completion: Completion, + ) -> Option { + match language_server_id.as_ref() { + Solargraph::LANGUAGE_SERVER_ID => { + self.solargraph.as_ref()?.label_for_completion(completion) + } + _ => None, + } + } +} + +zed::register_extension!(RubyExtension);