mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
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 <elliott.codes@gmail.com>
This commit is contained in:
parent
df00854bbc
commit
400e938997
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
16
extensions/ruby/Cargo.toml
Normal file
16
extensions/ruby/Cargo.toml
Normal file
@ -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"
|
1
extensions/ruby/LICENSE-APACHE
Symbolic link
1
extensions/ruby/LICENSE-APACHE
Symbolic link
@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
15
extensions/ruby/extension.toml
Normal file
15
extensions/ruby/extension.toml
Normal file
@ -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"
|
3
extensions/ruby/src/language_servers.rs
Normal file
3
extensions/ruby/src/language_servers.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod solargraph;
|
||||
|
||||
pub use solargraph::*;
|
121
extensions/ruby/src/language_servers/solargraph.rs
Normal file
121
extensions/ruby/src/language_servers/solargraph.rs
Normal file
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
62
extensions/ruby/src/ruby.rs
Normal file
62
extensions/ruby/src/ruby.rs
Normal file
@ -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);
|
Loading…
Reference in New Issue
Block a user