diff --git a/assets/settings/default.json b/assets/settings/default.json index c4e228fcd8..e4abbc0eb0 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -648,7 +648,7 @@ "tab_size": 2 }, "Ruby": { - "language_servers": ["solargraph", "..."] + "language_servers": ["solargraph", "!ruby-lsp", "..."] } }, // Zed's Prettier integration settings. diff --git a/extensions/ruby/extension.toml b/extensions/ruby/extension.toml index bb2658b62a..2753d2bd1e 100644 --- a/extensions/ruby/extension.toml +++ b/extensions/ruby/extension.toml @@ -10,6 +10,10 @@ repository = "https://github.com/zed-industries/zed" name = "Solargraph" language = "Ruby" +[language_servers.ruby-lsp] +name = "Ruby LSP" +language = "Ruby" + [grammars.ruby] repository = "https://github.com/tree-sitter/tree-sitter-ruby" commit = "9d86f3761bb30e8dcc81e754b81d3ce91848477e" diff --git a/extensions/ruby/src/language_servers.rs b/extensions/ruby/src/language_servers.rs index bd6252d586..f2afc44e88 100644 --- a/extensions/ruby/src/language_servers.rs +++ b/extensions/ruby/src/language_servers.rs @@ -1,3 +1,5 @@ +mod ruby_lsp; mod solargraph; +pub use ruby_lsp::*; pub use solargraph::*; diff --git a/extensions/ruby/src/language_servers/ruby_lsp.rs b/extensions/ruby/src/language_servers/ruby_lsp.rs new file mode 100644 index 0000000000..fcac423284 --- /dev/null +++ b/extensions/ruby/src/language_servers/ruby_lsp.rs @@ -0,0 +1,85 @@ +use zed::{ + lsp::{Completion, CompletionKind, Symbol, SymbolKind}, + CodeLabel, CodeLabelSpan, +}; +use zed_extension_api::{self as zed, Result}; + +pub struct RubyLsp {} + +impl RubyLsp { + pub const LANGUAGE_SERVER_ID: &'static str = "ruby-lsp"; + + pub fn new() -> Self { + Self {} + } + + pub fn server_script_path(&mut self, worktree: &zed::Worktree) -> Result { + let path = worktree.which("ruby-lsp").ok_or_else(|| { + "ruby-lsp must be installed manually. Install it with `gem install ruby-lsp`." + .to_string() + })?; + + Ok(path) + } + + pub fn label_for_completion(&self, completion: Completion) -> Option { + let highlight_name = match completion.kind? { + CompletionKind::Class | CompletionKind::Module => "type", + CompletionKind::Constant => "constant", + CompletionKind::Method => "function.method", + CompletionKind::Reference => "function.method", + CompletionKind::Keyword => "keyword", + _ => 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: vec![name_span], + filter_range: (0..len).into(), + }) + } + + pub fn label_for_symbol(&self, symbol: Symbol) -> Option { + let name = &symbol.name; + + return match symbol.kind { + SymbolKind::Method => { + let code = format!("def {name}; end"); + let filter_range = 0..name.len(); + let display_range = 4..4 + name.len(); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range(display_range)], + filter_range: filter_range.into(), + }) + } + SymbolKind::Class | SymbolKind::Module => { + let code = format!("class {name}; end"); + let filter_range = 0..name.len(); + let display_range = 6..6 + 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 index 18795ba295..6956230455 100644 --- a/extensions/ruby/src/ruby.rs +++ b/extensions/ruby/src/ruby.rs @@ -1,18 +1,23 @@ mod language_servers; use zed::lsp::{Completion, Symbol}; -use zed::{CodeLabel, LanguageServerId}; +use zed::serde_json::json; +use zed::{serde_json, CodeLabel, LanguageServerId}; use zed_extension_api::{self as zed, Result}; -use crate::language_servers::Solargraph; +use crate::language_servers::{RubyLsp, Solargraph}; struct RubyExtension { solargraph: Option, + ruby_lsp: Option, } impl zed::Extension for RubyExtension { fn new() -> Self { - Self { solargraph: None } + Self { + solargraph: None, + ruby_lsp: None, + } } fn language_server_command( @@ -30,6 +35,15 @@ impl zed::Extension for RubyExtension { env: worktree.shell_env(), }) } + RubyLsp::LANGUAGE_SERVER_ID => { + let ruby_lsp = self.ruby_lsp.get_or_insert_with(|| RubyLsp::new()); + + Ok(zed::Command { + command: ruby_lsp.server_script_path(worktree)?, + args: vec![], + env: worktree.shell_env(), + }) + } language_server_id => Err(format!("unknown language server: {language_server_id}")), } } @@ -41,6 +55,7 @@ impl zed::Extension for RubyExtension { ) -> Option { match language_server_id.as_ref() { Solargraph::LANGUAGE_SERVER_ID => self.solargraph.as_ref()?.label_for_symbol(symbol), + RubyLsp::LANGUAGE_SERVER_ID => self.ruby_lsp.as_ref()?.label_for_symbol(symbol), _ => None, } } @@ -54,9 +69,29 @@ impl zed::Extension for RubyExtension { Solargraph::LANGUAGE_SERVER_ID => { self.solargraph.as_ref()?.label_for_completion(completion) } + RubyLsp::LANGUAGE_SERVER_ID => self.ruby_lsp.as_ref()?.label_for_completion(completion), _ => None, } } + + fn language_server_initialization_options( + &mut self, + language_server_id: &LanguageServerId, + _worktree: &zed::Worktree, + ) -> Result> { + match language_server_id.as_ref() { + // We disable diagnostics because ruby-lsp uses pull-based diagnostics, + // which Zed doesn't support yet. + RubyLsp::LANGUAGE_SERVER_ID => Ok(Some(json!({ + "enabledFeatures": { + "diagnostics": false + }, + "experimentalFeaturesEnabled": true + }))), + + _ => Ok(None), + } + } } zed::register_extension!(RubyExtension);