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:
parent
df00854bbc
commit
400e938997
23 changed files with 230 additions and 211 deletions
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"
|
9
extensions/ruby/languages/erb/config.toml
Normal file
9
extensions/ruby/languages/erb/config.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
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"]
|
12
extensions/ruby/languages/erb/highlights.scm
Normal file
12
extensions/ruby/languages/erb/highlights.scm
Normal file
|
@ -0,0 +1,12 @@
|
|||
(comment_directive) @comment
|
||||
|
||||
[
|
||||
"<%#"
|
||||
"<%"
|
||||
"<%="
|
||||
"<%_"
|
||||
"<%-"
|
||||
"%>"
|
||||
"-%>"
|
||||
"_%>"
|
||||
] @keyword
|
7
extensions/ruby/languages/erb/injections.scm
Normal file
7
extensions/ruby/languages/erb/injections.scm
Normal file
|
@ -0,0 +1,7 @@
|
|||
((code) @content
|
||||
(#set! "language" "ruby")
|
||||
(#set! "combined"))
|
||||
|
||||
((content) @content
|
||||
(#set! "language" "html")
|
||||
(#set! "combined"))
|
14
extensions/ruby/languages/ruby/brackets.scm
Normal file
14
extensions/ruby/languages/ruby/brackets.scm
Normal file
|
@ -0,0 +1,14 @@
|
|||
("[" @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)
|
40
extensions/ruby/languages/ruby/config.toml
Normal file
40
extensions/ruby/languages/ruby/config.toml
Normal file
|
@ -0,0 +1,40 @@
|
|||
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 = "# ..."
|
22
extensions/ruby/languages/ruby/embedding.scm
Normal file
22
extensions/ruby/languages/ruby/embedding.scm
Normal file
|
@ -0,0 +1,22 @@
|
|||
(
|
||||
(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
|
||||
)
|
202
extensions/ruby/languages/ruby/highlights.scm
Normal file
202
extensions/ruby/languages/ruby/highlights.scm
Normal file
|
@ -0,0 +1,202 @@
|
|||
; 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
|
17
extensions/ruby/languages/ruby/indents.scm
Normal file
17
extensions/ruby/languages/ruby/indents.scm
Normal file
|
@ -0,0 +1,17 @@
|
|||
(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
|
20
extensions/ruby/languages/ruby/outline.scm
Normal file
20
extensions/ruby/languages/ruby/outline.scm
Normal file
|
@ -0,0 +1,20 @@
|
|||
(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
|
3
extensions/ruby/languages/ruby/overrides.scm
Normal file
3
extensions/ruby/languages/ruby/overrides.scm
Normal file
|
@ -0,0 +1,3 @@
|
|||
(comment) @comment
|
||||
(string) @string
|
||||
[(simple_symbol) (delimited_symbol)] @simple_symbol
|
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…
Add table
Add a link
Reference in a new issue