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:
Vitaly Slobodin 2024-05-10 17:53:11 +02:00 committed by GitHub
parent df00854bbc
commit 400e938997
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 230 additions and 211 deletions

View file

@ -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

View file

@ -1,9 +0,0 @@
name = "ERB"
grammar = "embedded_template"
path_suffixes = ["erb"]
autoclose_before = ">})"
brackets = [
{ start = "<", end = ">", close = true, newline = true },
]
block_comment = ["<%#", "%>"]
scope_opt_in_language_servers = ["tailwindcss-language-server"]

View file

@ -1,12 +0,0 @@
(comment_directive) @comment
[
"<%#"
"<%"
"<%="
"<%_"
"<%-"
"%>"
"-%>"
"_%>"
] @keyword

View file

@ -1,7 +0,0 @@
((code) @content
(#set! "language" "ruby")
(#set! "combined"))
((content) @content
(#set! "language" "html")
(#set! "combined"))

View file

@ -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",

View file

@ -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,
}
}
}

View file

@ -1,14 +0,0 @@
("[" @open "]" @close)
("{" @open "}" @close)
("\"" @open "\"" @close)
("do" @open "end" @close)
(block_parameters "|" @open "|" @close)
(interpolation "#{" @open "}" @close)
(if "if" @open "end" @close)
(unless "unless" @open "end" @close)
(begin "begin" @open "end" @close)
(module "module" @open "end" @close)
(_ . "def" @open "end" @close)
(_ . "class" @open "end" @close)

View file

@ -1,40 +0,0 @@
name = "Ruby"
grammar = "ruby"
path_suffixes = [
"rb",
"Gemfile",
"Guardfile",
"rake",
"Rakefile",
"ru",
"thor",
"cap",
"capfile",
"Capfile",
"jbuilder",
"rabl",
"rxml",
"builder",
"gemspec",
"rdoc",
"thor",
"pryrc",
"simplecov",
]
first_line_pattern = '^#!.*\bruby\b'
line_comments = ["# "]
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
{ start = "\"", end = "\"", close = true, newline = false, not_in = [
"comment",
"string",
] },
{ start = "'", end = "'", close = true, newline = false, not_in = [
"comment",
"string",
] },
]
collapsed_placeholder = "# ..."

View file

@ -1,22 +0,0 @@
(
(comment)* @context
.
[
(module
"module" @name
name: (_) @name)
(method
"def" @name
name: (_) @name
body: (body_statement) @collapse)
(class
"class" @name
name: (_) @name)
(singleton_method
"def" @name
object: (_) @name
"." @name
name: (_) @name
body: (body_statement) @collapse)
] @item
)

View file

@ -1,202 +0,0 @@
; Keywords
[
"alias"
"and"
"begin"
"break"
"case"
"class"
"def"
"do"
"else"
"elsif"
"end"
"ensure"
"for"
"if"
"in"
"module"
"next"
"or"
"rescue"
"retry"
"return"
"then"
"unless"
"until"
"when"
"while"
"yield"
] @keyword
((identifier) @keyword
(#match? @keyword "^(private|protected|public)$"))
; Function calls
((identifier) @function.method.builtin
(#eq? @function.method.builtin "require"))
"defined?" @function.method.builtin
(call
method: [(identifier) (constant)] @function.method)
; Function definitions
(alias (identifier) @function.method)
(setter (identifier) @function.method)
(method name: [(identifier) (constant)] @function.method)
(singleton_method name: [(identifier) (constant)] @function.method)
(method_parameters [
(identifier) @variable.parameter
(optional_parameter name: (identifier) @variable.parameter)
(keyword_parameter [name: (identifier) (":")] @variable.parameter)
])
(block_parameters (identifier) @variable.parameter)
; Identifiers
((identifier) @constant.builtin
(#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$"))
(file) @constant.builtin
(line) @constant.builtin
(encoding) @constant.builtin
(hash_splat_nil
"**" @operator
) @constant.builtin
(global_variable) @constant
(constant) @type
((constant) @constant
(#match? @constant "^[A-Z\\d_]+$"))
(superclass
(constant) @type.super)
(superclass
(scope_resolution
(constant) @type.super))
(superclass
(scope_resolution
(scope_resolution
(constant) @type.super)))
(self) @variable.special
(super) @variable.special
[
(class_variable)
(instance_variable)
] @variable.member
; Literals
[
(string)
(bare_string)
(subshell)
(heredoc_body)
(heredoc_beginning)
] @string
[
(simple_symbol)
(delimited_symbol)
(hash_key_symbol)
(bare_symbol)
] @string.special.symbol
(regex) @string.regex
(escape_sequence) @escape
[
(integer)
(float)
] @number
[
(nil)
(true)
(false)
] @constant.builtin
(comment) @comment
; Operators
[
"!"
"~"
"+"
"-"
"**"
"*"
"/"
"%"
"<<"
">>"
"&"
"|"
"^"
">"
"<"
"<="
">="
"=="
"!="
"=~"
"!~"
"<=>"
"||"
"&&"
".."
"..."
"="
"**="
"*="
"/="
"%="
"+="
"-="
"<<="
">>="
"&&="
"&="
"||="
"|="
"^="
"=>"
"->"
(operator)
] @operator
[
","
";"
"."
"::"
] @punctuation.delimiter
[
"("
")"
"["
"]"
"{"
"}"
"%w("
"%i("
] @punctuation.bracket
(interpolation
"#{" @punctuation.special
"}" @punctuation.special) @embedded

View file

@ -1,17 +0,0 @@
(method "end" @end) @indent
(class "end" @end) @indent
(module "end" @end) @indent
(begin "end" @end) @indent
(do_block "end" @end) @indent
(then) @indent
(call) @indent
(ensure) @outdent
(rescue) @outdent
(else) @outdent
(_ "[" "]" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent

View file

@ -1,20 +0,0 @@
(class
"class" @context
name: (_) @name) @item
((identifier) @context
(#match? @context "^(private|protected|public)$")) @item
(method
"def" @context
name: (_) @name) @item
(singleton_method
"def" @context
object: (_) @context
"." @context
name: (_) @name) @item
(module
"module" @context
name: (_) @name) @item

View file

@ -1,3 +0,0 @@
(comment) @comment
(string) @string
[(simple_symbol) (delimited_symbol)] @simple_symbol