diff --git a/Cargo.lock b/Cargo.lock index 680e40a7f9..513862d2ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5868,6 +5868,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-elixir" +version = "0.19.0" +source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=05e3631c6a0701c1fa518b0fee7be95a2ceef5e2#05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-go" version = "0.19.1" @@ -7056,6 +7065,7 @@ dependencies = [ "tree-sitter", "tree-sitter-c", "tree-sitter-cpp", + "tree-sitter-elixir", "tree-sitter-go", "tree-sitter-json 0.20.0", "tree-sitter-markdown", diff --git a/assets/settings/default.json b/assets/settings/default.json index 8ea515e7a9..95ba08c9d5 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -145,6 +145,9 @@ "C++": { "tab_size": 2 }, + "Elixir": { + "tab_size": 2 + }, "Go": { "tab_size": 4, "hard_tabs": true diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0f762f822f..dcab44e373 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -202,6 +202,7 @@ pub enum Event { pub enum LanguageServerState { Starting(Task>>), Running { + language: Arc, adapter: Arc, server: Arc, }, @@ -1969,7 +1970,7 @@ impl Project { uri: lsp::Url::from_file_path(abs_path).unwrap(), }; - for (_, server) in self.language_servers_for_worktree(worktree_id) { + for (_, _, server) in self.language_servers_for_worktree(worktree_id) { server .notify::( lsp::DidSaveTextDocumentParams { @@ -2004,15 +2005,18 @@ impl Project { fn language_servers_for_worktree( &self, worktree_id: WorktreeId, - ) -> impl Iterator, &Arc)> { + ) -> impl Iterator, &Arc, &Arc)> { self.language_server_ids .iter() .filter_map(move |((language_server_worktree_id, _), id)| { if *language_server_worktree_id == worktree_id { - if let Some(LanguageServerState::Running { adapter, server }) = - self.language_servers.get(id) + if let Some(LanguageServerState::Running { + adapter, + language, + server, + }) = self.language_servers.get(id) { - return Some((adapter, server)); + return Some((adapter, language, server)); } } None @@ -2282,6 +2286,7 @@ impl Project { server_id, LanguageServerState::Running { adapter: adapter.clone(), + language, server: language_server.clone(), }, ); @@ -3314,10 +3319,14 @@ impl Project { .worktree_for_id(worktree_id, cx) .and_then(|worktree| worktree.read(cx).as_local()) { - if let Some(LanguageServerState::Running { adapter, server }) = - self.language_servers.get(server_id) + if let Some(LanguageServerState::Running { + adapter, + language, + server, + }) = self.language_servers.get(server_id) { let adapter = adapter.clone(); + let language = language.clone(); let worktree_abs_path = worktree.abs_path().clone(); requests.push( server @@ -3331,6 +3340,7 @@ impl Project { .map(move |response| { ( adapter, + language, worktree_id, worktree_abs_path, response.unwrap_or_default(), @@ -3350,7 +3360,14 @@ impl Project { }; let symbols = this.read_with(&cx, |this, cx| { let mut symbols = Vec::new(); - for (adapter, source_worktree_id, worktree_abs_path, response) in responses { + for ( + adapter, + adapter_language, + source_worktree_id, + worktree_abs_path, + response, + ) in responses + { symbols.extend(response.into_iter().flatten().filter_map(|lsp_symbol| { let abs_path = lsp_symbol.location.uri.to_file_path().ok()?; let mut worktree_id = source_worktree_id; @@ -3369,16 +3386,15 @@ impl Project { path: path.into(), }; let signature = this.symbol_signature(&project_path); - let language = this.languages.select_language(&project_path.path); + let language = this + .languages + .select_language(&project_path.path) + .unwrap_or(adapter_language.clone()); let language_server_name = adapter.name.clone(); Some(async move { - let label = if let Some(language) = language { - language - .label_for_symbol(&lsp_symbol.name, lsp_symbol.kind) - .await - } else { - None - }; + let label = language + .label_for_symbol(&lsp_symbol.name, lsp_symbol.kind) + .await; Symbol { language_server_name, @@ -5940,8 +5956,9 @@ impl Project { let key = (worktree_id, name); if let Some(server_id) = self.language_server_ids.get(&key) { - if let Some(LanguageServerState::Running { adapter, server }) = - self.language_servers.get(server_id) + if let Some(LanguageServerState::Running { + adapter, server, .. + }) = self.language_servers.get(server_id) { return Some((adapter, server)); } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index aa31130d1e..153e87c2dd 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -91,6 +91,7 @@ toml = "0.5" tree-sitter = "0.20" tree-sitter-c = "0.20.1" tree-sitter-cpp = "0.20.0" +tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" } tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" } tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8" } tree-sitter-rust = "0.20.1" diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 8dc20bdbd1..857783062a 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -5,6 +5,7 @@ use rust_embed::RustEmbed; use std::{borrow::Cow, str, sync::Arc}; mod c; +mod elixir; mod go; mod installation; mod json; @@ -45,6 +46,11 @@ pub async fn init(languages: Arc, _executor: Arc) tree_sitter_cpp::language(), Some(CachedLspAdapter::new(c::CLspAdapter).await), ), + ( + "elixir", + tree_sitter_elixir::language(), + Some(CachedLspAdapter::new(elixir::ElixirLspAdapter).await), + ), ( "go", tree_sitter_go::language(), diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs new file mode 100644 index 0000000000..4959338522 --- /dev/null +++ b/crates/zed/src/languages/elixir.rs @@ -0,0 +1,195 @@ +use super::installation::{latest_github_release, GitHubLspBinaryVersion}; +use anyhow::{anyhow, Context, Result}; +use async_trait::async_trait; +use client::http::HttpClient; +use futures::StreamExt; +pub use language::*; +use lsp::{CompletionItemKind, SymbolKind}; +use smol::fs::{self, File}; +use std::{any::Any, path::PathBuf, sync::Arc}; +use util::ResultExt; + +pub struct ElixirLspAdapter; + +#[async_trait] +impl LspAdapter for ElixirLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("elixir-ls".into()) + } + + async fn fetch_latest_server_version( + &self, + http: Arc, + ) -> Result> { + let release = latest_github_release("elixir-lsp/elixir-ls", http).await?; + let asset_name = "elixir-ls.zip"; + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + let version = GitHubLspBinaryVersion { + name: release.name, + url: asset.browser_download_url.clone(), + }; + Ok(Box::new(version) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + http: Arc, + container_dir: PathBuf, + ) -> Result { + let version = version.downcast::().unwrap(); + let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name)); + let version_dir = container_dir.join(format!("elixir-ls_{}", version.name)); + let binary_path = version_dir.join("language_server.sh"); + + if fs::metadata(&binary_path).await.is_err() { + let mut response = http + .get(&version.url, Default::default(), true) + .await + .context("error downloading release")?; + let mut file = File::create(&zip_path) + .await + .with_context(|| format!("failed to create file {}", zip_path.display()))?; + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + futures::io::copy(response.body_mut(), &mut file).await?; + + fs::create_dir_all(&version_dir) + .await + .with_context(|| format!("failed to create directory {}", version_dir.display()))?; + let unzip_status = smol::process::Command::new("unzip") + .arg(&zip_path) + .arg("-d") + .arg(&version_dir) + .output() + .await? + .status; + if !unzip_status.success() { + Err(anyhow!("failed to unzip clangd archive"))?; + } + + if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if entry_path.as_path() != version_dir { + if let Ok(metadata) = fs::metadata(&entry_path).await { + if metadata.is_file() { + fs::remove_file(&entry_path).await.log_err(); + } else { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } + } + } + + Ok(binary_path) + } + + async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + (|| async move { + let mut last = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + last = Some(entry?.path()); + } + last.ok_or_else(|| anyhow!("no cached binary")) + })() + .await + .log_err() + } + + async fn label_for_completion( + &self, + completion: &lsp::CompletionItem, + language: &Language, + ) -> Option { + match completion.kind.zip(completion.detail.as_ref()) { + Some((_, detail)) if detail.starts_with("(function)") => { + let text = detail.strip_prefix("(function) ")?; + let filter_range = 0..text.find('(').unwrap_or(text.len()); + let source = Rope::from(format!("def {text}").as_str()); + let runs = language.highlight_text(&source, 4..4 + text.len()); + return Some(CodeLabel { + text: text.to_string(), + runs, + filter_range, + }); + } + Some((_, detail)) if detail.starts_with("(macro)") => { + let text = detail.strip_prefix("(macro) ")?; + let filter_range = 0..text.find('(').unwrap_or(text.len()); + let source = Rope::from(format!("defmacro {text}").as_str()); + let runs = language.highlight_text(&source, 9..9 + text.len()); + return Some(CodeLabel { + text: text.to_string(), + runs, + filter_range, + }); + } + Some(( + CompletionItemKind::CLASS + | CompletionItemKind::MODULE + | CompletionItemKind::INTERFACE + | CompletionItemKind::STRUCT, + _, + )) => { + let filter_range = 0..completion + .label + .find(" (") + .unwrap_or(completion.label.len()); + let text = &completion.label[filter_range.clone()]; + let source = Rope::from(format!("defmodule {text}").as_str()); + let runs = language.highlight_text(&source, 10..10 + text.len()); + return Some(CodeLabel { + text: completion.label.clone(), + runs, + filter_range, + }); + } + _ => {} + } + + None + } + + async fn label_for_symbol( + &self, + name: &str, + kind: SymbolKind, + language: &Language, + ) -> Option { + let (text, filter_range, display_range) = match kind { + SymbolKind::METHOD | SymbolKind::FUNCTION => { + let text = format!("def {}", name); + let filter_range = 4..4 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => { + let text = format!("defmodule {}", name); + let filter_range = 10..10 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + _ => return None, + }; + + Some(CodeLabel { + runs: language.highlight_text(&text.as_str().into(), display_range.clone()), + text: text[display_range].to_string(), + filter_range, + }) + } +} diff --git a/crates/zed/src/languages/elixir/brackets.scm b/crates/zed/src/languages/elixir/brackets.scm new file mode 100644 index 0000000000..d8713187e2 --- /dev/null +++ b/crates/zed/src/languages/elixir/brackets.scm @@ -0,0 +1,5 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) +("\"" @open "\"" @close) +("do" @open "end" @close) diff --git a/crates/zed/src/languages/elixir/config.toml b/crates/zed/src/languages/elixir/config.toml new file mode 100644 index 0000000000..4e1af93943 --- /dev/null +++ b/crates/zed/src/languages/elixir/config.toml @@ -0,0 +1,10 @@ +name = "Elixir" +path_suffixes = ["ex", "exs"] +line_comment = "# " +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 } +] diff --git a/crates/zed/src/languages/elixir/highlights.scm b/crates/zed/src/languages/elixir/highlights.scm new file mode 100644 index 0000000000..5c256f341c --- /dev/null +++ b/crates/zed/src/languages/elixir/highlights.scm @@ -0,0 +1,155 @@ +["when" "and" "or" "not" "in" "not in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword + +(unary_operator + operator: "@" @comment.doc + operand: (call + target: (identifier) @comment.doc.__attribute__ + (arguments + [ + (string) @comment.doc + (charlist) @comment.doc + (sigil + quoted_start: _ @comment.doc + quoted_end: _ @comment.doc) @comment.doc + (boolean) @comment.doc + ])) + (#match? @comment.doc.__attribute__ "^(moduledoc|typedoc|doc)$")) + +(unary_operator + operator: "&" + operand: (integer) @operator) + +(operator_identifier) @operator + +(unary_operator + operator: _ @operator) + +(binary_operator + operator: _ @operator) + +(dot + operator: _ @operator) + +(stab_clause + operator: _ @operator) + +[ + (boolean) + (nil) +] @constant + +[ + (integer) + (float) +] @number + +(alias) @type + +(call + target: (dot + left: (atom) @type)) + +(char) @constant + +(interpolation "#{" @punctuation.special "}" @punctuation.special) @embedded + +(escape_sequence) @string.escape + +[ + (atom) + (quoted_atom) + (keyword) + (quoted_keyword) +] @string.special.symbol + +[ + (string) + (charlist) +] @string + +(sigil + (sigil_name) @__name__ + quoted_start: _ @string + quoted_end: _ @string + (#match? @__name__ "^[sS]$")) @string + +(sigil + (sigil_name) @__name__ + quoted_start: _ @string.regex + quoted_end: _ @string.regex + (#match? @__name__ "^[rR]$")) @string.regex + +(sigil + (sigil_name) @__name__ + quoted_start: _ @string.special + quoted_end: _ @string.special) @string.special + +(call + target: [ + (identifier) @function + (dot + right: (identifier) @function) + ]) + +(call + target: (identifier) @keyword + (arguments + [ + (identifier) @function + (binary_operator + left: (identifier) @function + operator: "when") + ]) + (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$")) + +(call + target: (identifier) @keyword + (arguments + (binary_operator + operator: "|>" + right: (identifier))) + (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$")) + +(binary_operator + operator: "|>" + right: (identifier) @function) + +(call + target: (identifier) @keyword + (#match? @keyword "^(def|defdelegate|defexception|defguard|defguardp|defimpl|defmacro|defmacrop|defmodule|defn|defnp|defoverridable|defp|defprotocol|defstruct)$")) + +(call + target: (identifier) @keyword + (#match? @keyword "^(alias|case|cond|else|for|if|import|quote|raise|receive|require|reraise|super|throw|try|unless|unquote|unquote_splicing|use|with)$")) + +( + (identifier) @constant.builtin + (#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$") +) + +( + (identifier) @comment.unused + (#match? @comment.unused "^_") +) + +(comment) @comment + +[ + "%" +] @punctuation + +[ + "," + ";" +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" + "<<" + ">>" +] @punctuation.bracket diff --git a/crates/zed/src/languages/elixir/indents.scm b/crates/zed/src/languages/elixir/indents.scm new file mode 100644 index 0000000000..e4139841fc --- /dev/null +++ b/crates/zed/src/languages/elixir/indents.scm @@ -0,0 +1,8 @@ +[ + (call) +] @indent + +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent +(_ "do" "end" @end) @indent diff --git a/crates/zed/src/languages/elixir/outline.scm b/crates/zed/src/languages/elixir/outline.scm new file mode 100644 index 0000000000..985c8ffdca --- /dev/null +++ b/crates/zed/src/languages/elixir/outline.scm @@ -0,0 +1,16 @@ +(call + target: (identifier) @context + (arguments (alias) @name) + (#match? @context "^(defmodule|defprotocol)$")) @item + +(call + target: (identifier) @context + (arguments + [ + (identifier) @name + (call target: (identifier) @name) + (binary_operator + left: (call target: (identifier) @name) + operator: "when") + ]) + (#match? @context "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item