Detailed changes
@@ -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",
@@ -145,6 +145,9 @@
"C++": {
"tab_size": 2
},
+ "Elixir": {
+ "tab_size": 2
+ },
"Go": {
"tab_size": 4,
"hard_tabs": true
@@ -202,6 +202,7 @@ pub enum Event {
pub enum LanguageServerState {
Starting(Task<Option<Arc<LanguageServer>>>),
Running {
+ language: Arc<Language>,
adapter: Arc<CachedLspAdapter>,
server: Arc<LanguageServer>,
},
@@ -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::notification::DidSaveTextDocument>(
lsp::DidSaveTextDocumentParams {
@@ -2004,15 +2005,18 @@ impl Project {
fn language_servers_for_worktree(
&self,
worktree_id: WorktreeId,
- ) -> impl Iterator<Item = (&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> {
+ ) -> impl Iterator<Item = (&Arc<CachedLspAdapter>, &Arc<Language>, &Arc<LanguageServer>)> {
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));
}
@@ -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"
@@ -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<LanguageRegistry>, _executor: Arc<Background>)
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(),
@@ -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<dyn HttpClient>,
+ ) -> Result<Box<dyn 'static + Send + Any>> {
+ 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<dyn 'static + Send + Any>,
+ http: Arc<dyn HttpClient>,
+ container_dir: PathBuf,
+ ) -> Result<PathBuf> {
+ let version = version.downcast::<GitHubLspBinaryVersion>().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<PathBuf> {
+ (|| 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<CodeLabel> {
+ 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<CodeLabel> {
+ 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,
+ })
+ }
+}
@@ -0,0 +1,5 @@
+("(" @open ")" @close)
+("[" @open "]" @close)
+("{" @open "}" @close)
+("\"" @open "\"" @close)
+("do" @open "end" @close)
@@ -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 }
+]
@@ -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
@@ -0,0 +1,8 @@
+[
+ (call)
+] @indent
+
+(_ "[" "]" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent
+(_ "do" "end" @end) @indent
@@ -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