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.  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 <elliott.codes@gmail.com>
This commit is contained in:
1 parent
15bbf28116
commit
609f9d4bd3
23 files changed
+230
-211
No files matched your search
Generated
+7
-1
@@ -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"
|
||||
|
||||
@@ -127,6 +127,7 @@ members = [
|
||||
"extensions/php",
|
||||
"extensions/prisma",
|
||||
"extensions/purescript",
|
||||
"extensions/ruby",
|
||||
"extensions/svelte",
|
||||
"extensions/terraform",
|
||||
"extensions/toml",
|
||||
|
||||
@@ -633,6 +633,9 @@
|
||||
},
|
||||
"Prisma": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Ruby": {
|
||||
"language_servers": ["solargraph", "..."]
|
||||
}
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
|
||||
@@ -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"]),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -0,0 +1,15 @@
|
||||
id = "ruby"
|
||||
name = "Ruby"
|
||||
description = "Ruby support."
|
||||
version = "0.0.1"
|
||||
schema_version = 1
|
||||
authors = ["Vitaly Slobodin <vitaliy.slobodin@gmail.com>"]
|
||||
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"
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,3 @@
|
||||
mod solargraph;
|
||||
|
||||
pub use solargraph::*;
|
||||
@@ -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<String> {
|
||||
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<CodeLabel> {
|
||||
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<CodeLabel> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<Solargraph>,
|
||||
}
|
||||
|
||||
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<zed::Command> {
|
||||
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<CodeLabel> {
|
||||
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<CodeLabel> {
|
||||
match language_server_id.as_ref() {
|
||||
Solargraph::LANGUAGE_SERVER_ID => {
|
||||
self.solargraph.as_ref()?.label_for_completion(completion)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zed::register_extension!(RubyExtension);
|
||||
Reference in new issue
Block a user