diff --git a/Cargo.lock b/Cargo.lock index 20f15bb965..581375fae7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5546,12 +5546,9 @@ dependencies = [ "regex", "rope", "rust-embed", - "schemars", "serde", - "serde_derive", "serde_json", "settings", - "shellexpand", "smol", "task", "text", @@ -5562,12 +5559,10 @@ dependencies = [ "tree-sitter-c", "tree-sitter-cpp", "tree-sitter-css", - "tree-sitter-elixir", "tree-sitter-embedded-template", "tree-sitter-go", "tree-sitter-gomod", "tree-sitter-gowork", - "tree-sitter-heex", "tree-sitter-jsdoc", "tree-sitter-json 0.20.0", "tree-sitter-markdown", @@ -10513,7 +10508,7 @@ dependencies = [ [[package]] name = "tree-sitter" version = "0.20.100" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=7f21c3b98c0749ac192da67a0d65dfe3eabc4a63#7f21c3b98c0749ac192da67a0d65dfe3eabc4a63" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=528bcd2274814ca53711a57d71d1e3cf7abd73fe#528bcd2274814ca53711a57d71d1e3cf7abd73fe" dependencies = [ "cc", "regex", @@ -12730,6 +12725,13 @@ dependencies = [ "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "zed_elixir" +version = "0.0.1" +dependencies = [ + "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "zed_elm" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 5e2c1b27c5..0c4200f3e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,6 +110,7 @@ members = [ "extensions/csharp", "extensions/dart", "extensions/deno", + "extensions/elixir", "extensions/elm", "extensions/emmet", "extensions/erlang", @@ -406,7 +407,7 @@ features = [ ] [patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "7f21c3b98c0749ac192da67a0d65dfe3eabc4a63" } +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "528bcd2274814ca53711a57d71d1e3cf7abd73fe" } # Workaround for a broken nightly build of gpui: See #7644 and revisit once 0.5.3 is released. pathfinder_simd = { git = "https://github.com/servo/pathfinder.git", rev = "30419d07660dc11a21e42ef4a7fa329600cff152" } diff --git a/assets/settings/default.json b/assets/settings/default.json index 5e90ba524c..8b08aa3cf7 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -555,27 +555,6 @@ // Existing terminals will not pick up this change until they are recreated. // "max_scroll_history_lines": 10000, }, - // Settings specific to our elixir integration - "elixir": { - // Change the LSP zed uses for elixir. - // Note that changing this setting requires a restart of Zed - // to take effect. - // - // May take 3 values: - // 1. Use the standard ElixirLS, this is the default - // "lsp": "elixir_ls" - // 2. Use the experimental NextLs - // "lsp": "next_ls", - // 3. Use a language server installed locally on your machine: - // "lsp": { - // "local": { - // "path": "~/next-ls/bin/start", - // "arguments": ["--stdio"] - // } - // }, - // - "lsp": "elixir_ls" - }, "code_actions_on_format": {}, // An object whose keys are language names, and whose values // are arrays of filenames or extensions of files that should diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs index d4699fc798..d4f308e97d 100644 --- a/crates/extensions_ui/src/extension_suggest.rs +++ b/crates/extensions_ui/src/extension_suggest.rs @@ -21,6 +21,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[ ("dart", &["dart"]), ("dockerfile", &["Dockerfile"]), ("elisp", &["el"]), + ("elixir", &["ex", "exs", "heex"]), ("elm", &["elm"]), ("erlang", &["erl", "hrl"]), ("fish", &["fish"]), diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 0b299f171c..788ced8362 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -26,12 +26,9 @@ project.workspace = true regex.workspace = true rope.workspace = true rust-embed = "8.2.0" -schemars.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true settings.workspace = true -shellexpand.workspace = true smol.workspace = true task.workspace = true toml.workspace = true @@ -39,12 +36,10 @@ tree-sitter-bash.workspace = true tree-sitter-c.workspace = true tree-sitter-cpp.workspace = true tree-sitter-css.workspace = true -tree-sitter-elixir.workspace = true tree-sitter-embedded-template.workspace = true tree-sitter-go.workspace = true tree-sitter-gomod.workspace = true tree-sitter-gowork.workspace = true -tree-sitter-heex.workspace = true tree-sitter-jsdoc.workspace = true tree-sitter-json.workspace = true tree-sitter-markdown.workspace = true diff --git a/crates/languages/src/elixir.rs b/crates/languages/src/elixir.rs deleted file mode 100644 index 1a215bf34a..0000000000 --- a/crates/languages/src/elixir.rs +++ /dev/null @@ -1,607 +0,0 @@ -use anyhow::{anyhow, bail, Context, Result}; -use async_trait::async_trait; -use futures::StreamExt; -use gpui::{AppContext, AsyncAppContext, Task}; -pub use language::*; -use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind}; -use project::project_settings::ProjectSettings; -use schemars::JsonSchema; -use serde_derive::{Deserialize, Serialize}; -use serde_json::Value; -use settings::{Settings, SettingsSources}; -use smol::fs::{self, File}; -use std::{ - any::Any, - env::consts, - ops::Deref, - path::PathBuf, - sync::{ - atomic::{AtomicBool, Ordering::SeqCst}, - Arc, - }, -}; -use task::{TaskTemplate, TaskTemplates, VariableName}; -use util::{ - fs::remove_matching, - github::{latest_github_release, GitHubLspBinaryVersion}, - maybe, ResultExt, -}; - -#[derive(Clone, Serialize, Deserialize, JsonSchema)] -pub struct ElixirSettings { - pub lsp: ElixirLspSetting, -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ElixirLspSetting { - ElixirLs, - NextLs, - Local { - path: String, - arguments: Vec, - }, -} - -#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)] -pub struct ElixirSettingsContent { - lsp: Option, -} - -impl Settings for ElixirSettings { - const KEY: Option<&'static str> = Some("elixir"); - - type FileContent = ElixirSettingsContent; - - fn load(sources: SettingsSources, _: &mut AppContext) -> Result { - sources.json_merge() - } -} - -pub struct ElixirLspAdapter; - -#[async_trait(?Send)] -impl LspAdapter for ElixirLspAdapter { - fn name(&self) -> LanguageServerName { - LanguageServerName("elixir-ls".into()) - } - - fn will_start_server( - &self, - delegate: &Arc, - cx: &mut AsyncAppContext, - ) -> Option>> { - static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false); - - const NOTIFICATION_MESSAGE: &str = "Could not run the elixir language server, `elixir-ls`, because `elixir` was not found."; - - let delegate = delegate.clone(); - Some(cx.spawn(|cx| async move { - let elixir_output = smol::process::Command::new("elixir") - .args(["--version"]) - .output() - .await; - if elixir_output.is_err() { - if DID_SHOW_NOTIFICATION - .compare_exchange(false, true, SeqCst, SeqCst) - .is_ok() - { - cx.update(|cx| { - delegate.show_notification(NOTIFICATION_MESSAGE, cx); - })? - } - return Err(anyhow!("cannot run elixir-ls")); - } - - Ok(()) - })) - } - - async fn fetch_latest_server_version( - &self, - delegate: &dyn LspAdapterDelegate, - ) -> Result> { - let http = delegate.http_client(); - let release = latest_github_release("elixir-lsp/elixir-ls", true, false, http).await?; - - let asset_name = format!("elixir-ls-{}.zip", &release.tag_name); - 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.tag_name.clone(), - url: asset.browser_download_url.clone(), - }; - Ok(Box::new(version) as Box<_>) - } - - async fn fetch_server_binary( - &self, - version: Box, - container_dir: PathBuf, - delegate: &dyn LspAdapterDelegate, - ) -> Result { - let version = version.downcast::().unwrap(); - let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name)); - let folder_path = container_dir.join("elixir-ls"); - let binary_path = folder_path.join("language_server.sh"); - - if fs::metadata(&binary_path).await.is_err() { - let mut response = delegate - .http_client() - .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(&folder_path) - .await - .with_context(|| format!("failed to create directory {}", folder_path.display()))?; - let unzip_status = smol::process::Command::new("unzip") - .arg(&zip_path) - .arg("-d") - .arg(&folder_path) - .output() - .await? - .status; - if !unzip_status.success() { - Err(anyhow!("failed to unzip elixir-ls archive"))?; - } - - remove_matching(&container_dir, |entry| entry != folder_path).await; - } - - Ok(LanguageServerBinary { - path: binary_path, - env: None, - arguments: vec![], - }) - } - - async fn cached_server_binary( - &self, - container_dir: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - get_cached_server_binary_elixir_ls(container_dir).await - } - - async fn installation_test_binary( - &self, - container_dir: PathBuf, - ) -> Option { - get_cached_server_binary_elixir_ls(container_dir).await - } - - async fn label_for_completion( - &self, - completion: &lsp::CompletionItem, - language: &Arc, - ) -> 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: &Arc, - ) -> 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, - }) - } - - async fn workspace_configuration( - self: Arc, - _: &Arc, - cx: &mut AsyncAppContext, - ) -> Result { - let settings = cx.update(|cx| { - ProjectSettings::get_global(cx) - .lsp - .get("elixir-ls") - .and_then(|s| s.settings.clone()) - .unwrap_or_default() - })?; - - Ok(serde_json::json!({ - "elixirLS": settings - })) - } -} - -async fn get_cached_server_binary_elixir_ls( - container_dir: PathBuf, -) -> Option { - let server_path = container_dir.join("elixir-ls/language_server.sh"); - if server_path.exists() { - Some(LanguageServerBinary { - path: server_path, - env: None, - arguments: vec![], - }) - } else { - log::error!("missing executable in directory {:?}", server_path); - None - } -} - -pub struct NextLspAdapter; - -#[async_trait(?Send)] -impl LspAdapter for NextLspAdapter { - fn name(&self) -> LanguageServerName { - LanguageServerName("next-ls".into()) - } - - async fn fetch_latest_server_version( - &self, - delegate: &dyn LspAdapterDelegate, - ) -> Result> { - let platform = match consts::ARCH { - "x86_64" => "darwin_amd64", - "aarch64" => "darwin_arm64", - other => bail!("Running on unsupported platform: {other}"), - }; - let release = - latest_github_release("elixir-tools/next-ls", true, false, delegate.http_client()) - .await?; - let version = release.tag_name; - let asset_name = format!("next_ls_{platform}"); - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .with_context(|| format!("no asset found matching {asset_name:?}"))?; - let version = GitHubLspBinaryVersion { - name: version, - url: asset.browser_download_url.clone(), - }; - Ok(Box::new(version) as Box<_>) - } - - async fn fetch_server_binary( - &self, - version: Box, - container_dir: PathBuf, - delegate: &dyn LspAdapterDelegate, - ) -> Result { - let version = version.downcast::().unwrap(); - - let binary_path = container_dir.join("next-ls"); - - if fs::metadata(&binary_path).await.is_err() { - let mut response = delegate - .http_client() - .get(&version.url, Default::default(), true) - .await - .map_err(|err| anyhow!("error downloading release: {}", err))?; - - let mut file = smol::fs::File::create(&binary_path).await?; - if !response.status().is_success() { - Err(anyhow!( - "download failed with status {}", - response.status().to_string() - ))?; - } - futures::io::copy(response.body_mut(), &mut file).await?; - - // todo("windows") - #[cfg(not(windows))] - { - fs::set_permissions( - &binary_path, - ::from_mode(0o755), - ) - .await?; - } - } - - Ok(LanguageServerBinary { - path: binary_path, - env: None, - arguments: vec!["--stdio".into()], - }) - } - - async fn cached_server_binary( - &self, - container_dir: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - get_cached_server_binary_next(container_dir) - .await - .map(|mut binary| { - binary.arguments = vec!["--stdio".into()]; - binary - }) - } - - async fn installation_test_binary( - &self, - container_dir: PathBuf, - ) -> Option { - get_cached_server_binary_next(container_dir) - .await - .map(|mut binary| { - binary.arguments = vec!["--help".into()]; - binary - }) - } - - async fn label_for_completion( - &self, - completion: &lsp::CompletionItem, - language: &Arc, - ) -> Option { - label_for_completion_elixir(completion, language) - } - - async fn label_for_symbol( - &self, - name: &str, - symbol_kind: SymbolKind, - language: &Arc, - ) -> Option { - label_for_symbol_elixir(name, symbol_kind, language) - } -} - -async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option { - maybe!(async { - let mut last_binary_path = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - let entry = entry?; - if entry.file_type().await?.is_file() - && entry - .file_name() - .to_str() - .map_or(false, |name| name == "next-ls") - { - last_binary_path = Some(entry.path()); - } - } - - if let Some(path) = last_binary_path { - Ok(LanguageServerBinary { - path, - env: None, - arguments: Vec::new(), - }) - } else { - Err(anyhow!("no cached binary")) - } - }) - .await - .log_err() -} - -pub struct LocalLspAdapter { - pub path: String, - pub arguments: Vec, -} - -#[async_trait(?Send)] -impl LspAdapter for LocalLspAdapter { - fn name(&self) -> LanguageServerName { - LanguageServerName("local-ls".into()) - } - - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - ) -> Result> { - Ok(Box::new(()) as Box<_>) - } - - async fn fetch_server_binary( - &self, - _: Box, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Result { - let path = shellexpand::full(&self.path)?; - Ok(LanguageServerBinary { - path: PathBuf::from(path.deref()), - env: None, - arguments: self.arguments.iter().map(|arg| arg.into()).collect(), - }) - } - - async fn cached_server_binary( - &self, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - let path = shellexpand::full(&self.path).ok()?; - Some(LanguageServerBinary { - path: PathBuf::from(path.deref()), - env: None, - arguments: self.arguments.iter().map(|arg| arg.into()).collect(), - }) - } - - async fn installation_test_binary(&self, _: PathBuf) -> Option { - let path = shellexpand::full(&self.path).ok()?; - Some(LanguageServerBinary { - path: PathBuf::from(path.deref()), - env: None, - arguments: self.arguments.iter().map(|arg| arg.into()).collect(), - }) - } - - async fn label_for_completion( - &self, - completion: &lsp::CompletionItem, - language: &Arc, - ) -> Option { - label_for_completion_elixir(completion, language) - } - - async fn label_for_symbol( - &self, - name: &str, - symbol: SymbolKind, - language: &Arc, - ) -> Option { - label_for_symbol_elixir(name, symbol, language) - } -} - -fn label_for_completion_elixir( - completion: &lsp::CompletionItem, - language: &Arc, -) -> Option { - return Some(CodeLabel { - runs: language.highlight_text(&completion.label.clone().into(), 0..completion.label.len()), - text: completion.label.clone(), - filter_range: 0..completion.label.len(), - }); -} - -fn label_for_symbol_elixir( - name: &str, - _: SymbolKind, - language: &Arc, -) -> Option { - Some(CodeLabel { - runs: language.highlight_text(&name.into(), 0..name.len()), - text: name.to_string(), - filter_range: 0..name.len(), - }) -} - -pub(super) fn elixir_task_context() -> ContextProviderWithTasks { - // Taken from https://gist.github.com/josevalim/2e4f60a14ccd52728e3256571259d493#gistcomment-4995881 - ContextProviderWithTasks::new(TaskTemplates(vec![ - TaskTemplate { - label: "mix test".to_owned(), - command: "mix".to_owned(), - args: vec!["test".to_owned()], - ..TaskTemplate::default() - }, - TaskTemplate { - label: "mix test --failed".to_owned(), - command: "mix".to_owned(), - args: vec!["test".to_owned(), "--failed".to_owned()], - ..TaskTemplate::default() - }, - TaskTemplate { - label: format!("mix test {}", VariableName::Symbol.template_value()), - command: "mix".to_owned(), - args: vec!["test".to_owned(), VariableName::Symbol.template_value()], - ..TaskTemplate::default() - }, - TaskTemplate { - label: format!( - "mix test {}:{}", - VariableName::File.template_value(), - VariableName::Row.template_value() - ), - command: "mix".to_owned(), - args: vec![ - "test".to_owned(), - format!( - "{}:{}", - VariableName::File.template_value(), - VariableName::Row.template_value() - ), - ], - ..TaskTemplate::default() - }, - TaskTemplate { - label: "Elixir: break line".to_owned(), - command: "iex".to_owned(), - args: vec![ - "-S".to_owned(), - "mix".to_owned(), - "test".to_owned(), - "-b".to_owned(), - format!( - "{}:{}", - VariableName::File.template_value(), - VariableName::Row.template_value() - ), - ], - ..TaskTemplate::default() - }, - ])) -} diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index ff315727c4..ed9e8c351f 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -3,22 +3,16 @@ use gpui::{AppContext, BorrowAppContext}; pub use language::*; use node_runtime::NodeRuntime; use rust_embed::RustEmbed; -use settings::{Settings, SettingsStore}; +use settings::SettingsStore; use smol::stream::StreamExt; use std::{str, sync::Arc}; use util::{asset_str, ResultExt}; -use crate::{ - bash::bash_task_context, elixir::elixir_task_context, python::python_task_context, - rust::RustContextProvider, -}; - -use self::elixir::ElixirSettings; +use crate::{bash::bash_task_context, python::python_task_context, rust::RustContextProvider}; mod bash; mod c; mod css; -mod elixir; mod go; mod json; mod python; @@ -47,14 +41,11 @@ pub fn init( node_runtime: Arc, cx: &mut AppContext, ) { - ElixirSettings::register(cx); - languages.register_native_grammars([ ("bash", tree_sitter_bash::language()), ("c", tree_sitter_c::language()), ("cpp", tree_sitter_cpp::language()), ("css", tree_sitter_css::language()), - ("elixir", tree_sitter_elixir::language()), ( "embedded_template", tree_sitter_embedded_template::language(), @@ -62,7 +53,6 @@ pub fn init( ("go", tree_sitter_go::language()), ("gomod", tree_sitter_gomod::language()), ("gowork", tree_sitter_gowork::language()), - ("heex", tree_sitter_heex::language()), ("jsdoc", tree_sitter_jsdoc::language()), ("json", tree_sitter_json::language()), ("markdown", tree_sitter_markdown::language()), @@ -131,46 +121,9 @@ pub fn init( Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ] ); - - match &ElixirSettings::get(None, cx).lsp { - elixir::ElixirLspSetting::ElixirLs => { - language!( - "elixir", - vec![ - Arc::new(elixir::ElixirLspAdapter), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], - elixir_task_context() - ); - } - elixir::ElixirLspSetting::NextLs => { - language!( - "elixir", - vec![Arc::new(elixir::NextLspAdapter)], - elixir_task_context() - ); - } - elixir::ElixirLspSetting::Local { path, arguments } => { - language!( - "elixir", - vec![Arc::new(elixir::LocalLspAdapter { - path: path.clone(), - arguments: arguments.clone(), - })], - elixir_task_context() - ); - } - } language!("go", vec![Arc::new(go::GoLspAdapter)]); language!("gomod"); language!("gowork"); - language!( - "heex", - vec![ - Arc::new(elixir::ElixirLspAdapter), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ] - ); language!( "json", vec![Arc::new(json::JsonLspAdapter::new( @@ -232,6 +185,7 @@ pub fn init( let tailwind_languages = [ "Astro", + "HEEX", "HTML", "PHP", "Svelte", diff --git a/extensions/elixir/Cargo.toml b/extensions/elixir/Cargo.toml new file mode 100644 index 0000000000..8e8d6c0b2b --- /dev/null +++ b/extensions/elixir/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "zed_elixir" +version = "0.0.1" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/elixir.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = "0.0.6" diff --git a/extensions/elixir/LICENSE-APACHE b/extensions/elixir/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/extensions/elixir/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/elixir/extension.toml b/extensions/elixir/extension.toml new file mode 100644 index 0000000000..7631b0c535 --- /dev/null +++ b/extensions/elixir/extension.toml @@ -0,0 +1,27 @@ +id = "elixir" +name = "Elixir" +description = "Elixir support." +version = "0.0.1" +schema_version = 1 +authors = ["Marshall Bowers "] +repository = "https://github.com/zed-industries/zed" + +[language_servers.elixir-ls] +name = "ElixirLS" +languages = ["Elixir", "HEEX"] + +[language_servers.next-ls] +name = "Next LS" +languages = ["Elixir", "HEEX"] + +[language_servers.lexical] +name = "Lexical" +languages = ["Elixir", "HEEX"] + +[grammars.elixir] +repository = "https://github.com/elixir-lang/tree-sitter-elixir" +commit = "a2861e88a730287a60c11ea9299c033c7d076e30" + +[grammars.heex] +repository = "https://github.com/phoenixframework/tree-sitter-heex" +commit = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" diff --git a/crates/languages/src/elixir/brackets.scm b/extensions/elixir/languages/elixir/brackets.scm similarity index 100% rename from crates/languages/src/elixir/brackets.scm rename to extensions/elixir/languages/elixir/brackets.scm diff --git a/crates/languages/src/elixir/config.toml b/extensions/elixir/languages/elixir/config.toml similarity index 100% rename from crates/languages/src/elixir/config.toml rename to extensions/elixir/languages/elixir/config.toml diff --git a/crates/languages/src/elixir/embedding.scm b/extensions/elixir/languages/elixir/embedding.scm similarity index 100% rename from crates/languages/src/elixir/embedding.scm rename to extensions/elixir/languages/elixir/embedding.scm diff --git a/crates/languages/src/elixir/highlights.scm b/extensions/elixir/languages/elixir/highlights.scm similarity index 100% rename from crates/languages/src/elixir/highlights.scm rename to extensions/elixir/languages/elixir/highlights.scm diff --git a/crates/languages/src/elixir/indents.scm b/extensions/elixir/languages/elixir/indents.scm similarity index 100% rename from crates/languages/src/elixir/indents.scm rename to extensions/elixir/languages/elixir/indents.scm diff --git a/crates/languages/src/elixir/injections.scm b/extensions/elixir/languages/elixir/injections.scm similarity index 100% rename from crates/languages/src/elixir/injections.scm rename to extensions/elixir/languages/elixir/injections.scm diff --git a/crates/languages/src/elixir/outline.scm b/extensions/elixir/languages/elixir/outline.scm similarity index 100% rename from crates/languages/src/elixir/outline.scm rename to extensions/elixir/languages/elixir/outline.scm diff --git a/crates/languages/src/elixir/overrides.scm b/extensions/elixir/languages/elixir/overrides.scm similarity index 100% rename from crates/languages/src/elixir/overrides.scm rename to extensions/elixir/languages/elixir/overrides.scm diff --git a/extensions/elixir/languages/elixir/tasks.json b/extensions/elixir/languages/elixir/tasks.json new file mode 100644 index 0000000000..0e48fcfdc3 --- /dev/null +++ b/extensions/elixir/languages/elixir/tasks.json @@ -0,0 +1,28 @@ +// Taken from https://gist.github.com/josevalim/2e4f60a14ccd52728e3256571259d493#gistcomment-4995881 +[ + { + "label": "mix test", + "command": "mix", + "args": ["test"] + }, + { + "label": "mix test --failed", + "command": "mix", + "args": ["test", "--failed"] + }, + { + "label": "mix test $ZED_SYMBOL", + "command": "mix", + "args": ["test", "$ZED_SYMBOL"] + }, + { + "label": "mix test $ZED_FILE:$ZED_ROW", + "command": "mix", + "args": ["test", "$ZED_FILE:$ZED_ROW"] + }, + { + "label": "Elixir: break line", + "command": "iex", + "args": ["-S", "mix", "test", "-b", "$ZED_FILE:$ZED_ROW"] + } +] diff --git a/crates/languages/src/heex/config.toml b/extensions/elixir/languages/heex/config.toml similarity index 100% rename from crates/languages/src/heex/config.toml rename to extensions/elixir/languages/heex/config.toml diff --git a/crates/languages/src/heex/highlights.scm b/extensions/elixir/languages/heex/highlights.scm similarity index 100% rename from crates/languages/src/heex/highlights.scm rename to extensions/elixir/languages/heex/highlights.scm diff --git a/crates/languages/src/heex/injections.scm b/extensions/elixir/languages/heex/injections.scm similarity index 100% rename from crates/languages/src/heex/injections.scm rename to extensions/elixir/languages/heex/injections.scm diff --git a/crates/languages/src/heex/overrides.scm b/extensions/elixir/languages/heex/overrides.scm similarity index 100% rename from crates/languages/src/heex/overrides.scm rename to extensions/elixir/languages/heex/overrides.scm diff --git a/extensions/elixir/src/elixir.rs b/extensions/elixir/src/elixir.rs new file mode 100644 index 0000000000..b708cc8470 --- /dev/null +++ b/extensions/elixir/src/elixir.rs @@ -0,0 +1,107 @@ +mod language_servers; + +use zed::lsp::{Completion, Symbol}; +use zed::{serde_json, CodeLabel, LanguageServerId}; +use zed_extension_api::{self as zed, Result}; + +use crate::language_servers::{ElixirLs, Lexical, NextLs}; + +struct ElixirExtension { + elixir_ls: Option, + next_ls: Option, + lexical: Option, +} + +impl zed::Extension for ElixirExtension { + fn new() -> Self { + Self { + elixir_ls: None, + next_ls: None, + lexical: None, + } + } + + fn language_server_command( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + match language_server_id.as_ref() { + ElixirLs::LANGUAGE_SERVER_ID => { + let elixir_ls = self.elixir_ls.get_or_insert_with(|| ElixirLs::new()); + + Ok(zed::Command { + command: elixir_ls.language_server_binary_path(language_server_id, worktree)?, + args: vec![], + env: Default::default(), + }) + } + NextLs::LANGUAGE_SERVER_ID => { + let next_ls = self.next_ls.get_or_insert_with(|| NextLs::new()); + + Ok(zed::Command { + command: next_ls.language_server_binary_path(language_server_id, worktree)?, + args: vec!["--stdio".to_string()], + env: Default::default(), + }) + } + Lexical::LANGUAGE_SERVER_ID => { + let lexical = self.lexical.get_or_insert_with(|| Lexical::new()); + + Ok(zed::Command { + command: lexical.language_server_binary_path(language_server_id, worktree)?, + args: vec![], + env: Default::default(), + }) + } + language_server_id => Err(format!("unknown language server: {language_server_id}")), + } + } + + fn label_for_completion( + &self, + language_server_id: &LanguageServerId, + completion: Completion, + ) -> Option { + match language_server_id.as_ref() { + ElixirLs::LANGUAGE_SERVER_ID => { + self.elixir_ls.as_ref()?.label_for_completion(completion) + } + NextLs::LANGUAGE_SERVER_ID => self.next_ls.as_ref()?.label_for_completion(completion), + Lexical::LANGUAGE_SERVER_ID => self.lexical.as_ref()?.label_for_completion(completion), + _ => None, + } + } + + fn label_for_symbol( + &self, + language_server_id: &LanguageServerId, + symbol: Symbol, + ) -> Option { + match language_server_id.as_ref() { + ElixirLs::LANGUAGE_SERVER_ID => self.elixir_ls.as_ref()?.label_for_symbol(symbol), + NextLs::LANGUAGE_SERVER_ID => self.next_ls.as_ref()?.label_for_symbol(symbol), + Lexical::LANGUAGE_SERVER_ID => self.lexical.as_ref()?.label_for_symbol(symbol), + _ => None, + } + } + + fn language_server_initialization_options( + &mut self, + language_server_id: &LanguageServerId, + _worktree: &zed::Worktree, + ) -> Result> { + match language_server_id.as_ref() { + NextLs::LANGUAGE_SERVER_ID => Ok(Some(serde_json::json!({ + "experimental": { + "completions": { + "enable": true + } + } + }))), + _ => Ok(None), + } + } +} + +zed::register_extension!(ElixirExtension); diff --git a/extensions/elixir/src/language_servers.rs b/extensions/elixir/src/language_servers.rs new file mode 100644 index 0000000000..c2ce97e677 --- /dev/null +++ b/extensions/elixir/src/language_servers.rs @@ -0,0 +1,7 @@ +mod elixir_ls; +mod lexical; +mod next_ls; + +pub use elixir_ls::*; +pub use lexical::*; +pub use next_ls::*; diff --git a/extensions/elixir/src/language_servers/elixir_ls.rs b/extensions/elixir/src/language_servers/elixir_ls.rs new file mode 100644 index 0000000000..1bd179930b --- /dev/null +++ b/extensions/elixir/src/language_servers/elixir_ls.rs @@ -0,0 +1,165 @@ +use std::fs; + +use zed::lsp::{Completion, CompletionKind, Symbol, SymbolKind}; +use zed::{CodeLabel, CodeLabelSpan, LanguageServerId}; +use zed_extension_api::{self as zed, Result}; + +pub struct ElixirLs { + cached_binary_path: Option, +} + +impl ElixirLs { + pub const LANGUAGE_SERVER_ID: &'static str = "elixir-ls"; + + pub fn new() -> Self { + Self { + cached_binary_path: None, + } + } + + pub fn language_server_binary_path( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + if let Some(path) = worktree.which("elixir-ls") { + return Ok(path); + } + + if let Some(path) = &self.cached_binary_path { + if fs::metadata(path).map_or(false, |stat| stat.is_file()) { + return Ok(path.clone()); + } + } + + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let release = zed::latest_github_release( + "elixir-lsp/elixir-ls", + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let asset_name = format!("elixir-ls-{version}.zip", version = release.version,); + + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; + + let (platform, _arch) = zed::current_platform(); + let version_dir = format!("elixir-ls-{}", release.version); + let binary_path = format!( + "{version_dir}/language_server.{extension}", + extension = match platform { + zed::Os::Mac | zed::Os::Linux => "sh", + zed::Os::Windows => "bat", + } + ); + + if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + + zed::download_file( + &asset.download_url, + &version_dir, + zed::DownloadedFileType::Zip, + ) + .map_err(|e| format!("failed to download file: {e}"))?; + + let entries = + fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; + for entry in entries { + let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; + if entry.file_name().to_str() != Some(&version_dir) { + fs::remove_dir_all(&entry.path()).ok(); + } + } + } + + self.cached_binary_path = Some(binary_path.clone()); + Ok(binary_path) + } + + pub fn label_for_completion(&self, completion: Completion) -> Option { + match completion.kind? { + CompletionKind::Module + | CompletionKind::Class + | CompletionKind::Interface + | CompletionKind::Struct => { + let name = completion.label; + let defmodule = "defmodule "; + let code = format!("{defmodule}{name}"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range( + defmodule.len()..defmodule.len() + name.len(), + )], + filter_range: (0..name.len()).into(), + }) + } + CompletionKind::Function | CompletionKind::Constant => { + let name = completion.label; + let def = "def "; + let code = format!("{def}{name}"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range(def.len()..def.len() + name.len())], + filter_range: (0..name.len()).into(), + }) + } + CompletionKind::Operator => { + let name = completion.label; + let def_a = "def a "; + let code = format!("{def_a}{name} b"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range( + def_a.len()..def_a.len() + name.len(), + )], + filter_range: (0..name.len()).into(), + }) + } + _ => None, + } + } + + pub fn label_for_symbol(&self, symbol: Symbol) -> Option { + let name = &symbol.name; + + let (code, filter_range, display_range) = match symbol.kind { + SymbolKind::Module | SymbolKind::Class | SymbolKind::Interface | SymbolKind::Struct => { + let defmodule = "defmodule "; + let code = format!("{defmodule}{name}"); + let filter_range = 0..name.len(); + let display_range = defmodule.len()..defmodule.len() + name.len(); + (code, filter_range, display_range) + } + SymbolKind::Function | SymbolKind::Constant => { + let def = "def "; + let code = format!("{def}{name}"); + let filter_range = 0..name.len(); + let display_range = def.len()..def.len() + name.len(); + (code, filter_range, display_range) + } + _ => return None, + }; + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range(display_range)], + filter_range: filter_range.into(), + code, + }) + } +} diff --git a/extensions/elixir/src/language_servers/lexical.rs b/extensions/elixir/src/language_servers/lexical.rs new file mode 100644 index 0000000000..b15984498f --- /dev/null +++ b/extensions/elixir/src/language_servers/lexical.rs @@ -0,0 +1,130 @@ +use std::fs; + +use zed::lsp::{Completion, CompletionKind, Symbol}; +use zed::{CodeLabel, CodeLabelSpan, LanguageServerId}; +use zed_extension_api::{self as zed, Result}; + +pub struct Lexical { + cached_binary_path: Option, +} + +impl Lexical { + pub const LANGUAGE_SERVER_ID: &'static str = "lexical"; + + pub fn new() -> Self { + Self { + cached_binary_path: None, + } + } + + pub fn language_server_binary_path( + &mut self, + language_server_id: &LanguageServerId, + _worktree: &zed::Worktree, + ) -> Result { + if let Some(path) = &self.cached_binary_path { + if fs::metadata(path).map_or(false, |stat| stat.is_file()) { + return Ok(path.clone()); + } + } + + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let release = zed::latest_github_release( + "lexical-lsp/lexical", + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let asset_name = format!("lexical-{version}.zip", version = release.version); + + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; + + let version_dir = format!("lexical-{}", release.version); + let binary_path = format!("{version_dir}/lexical/bin/start_lexical.sh"); + + if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + + zed::download_file( + &asset.download_url, + &version_dir, + zed::DownloadedFileType::Zip, + ) + .map_err(|e| format!("failed to download file: {e}"))?; + + let entries = + fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; + for entry in entries { + let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; + if entry.file_name().to_str() != Some(&version_dir) { + fs::remove_dir_all(&entry.path()).ok(); + } + } + } + + self.cached_binary_path = Some(binary_path.clone()); + Ok(binary_path) + } + + pub fn label_for_completion(&self, completion: Completion) -> Option { + match completion.kind? { + CompletionKind::Module + | CompletionKind::Class + | CompletionKind::Interface + | CompletionKind::Struct => { + let name = completion.label; + let defmodule = "defmodule "; + let code = format!("{defmodule}{name}"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range( + defmodule.len()..defmodule.len() + name.len(), + )], + filter_range: (0..name.len()).into(), + }) + } + CompletionKind::Function | CompletionKind::Constant => { + let name = completion.label; + let def = "def "; + let code = format!("{def}{name}"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range(def.len()..def.len() + name.len())], + filter_range: (0..name.len()).into(), + }) + } + CompletionKind::Operator => { + let name = completion.label; + let def_a = "def a "; + let code = format!("{def_a}{name} b"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range( + def_a.len()..def_a.len() + name.len(), + )], + filter_range: (0..name.len()).into(), + }) + } + _ => None, + } + } + + pub fn label_for_symbol(&self, _symbol: Symbol) -> Option { + None + } +} diff --git a/extensions/elixir/src/language_servers/next_ls.rs b/extensions/elixir/src/language_servers/next_ls.rs new file mode 100644 index 0000000000..14c216f312 --- /dev/null +++ b/extensions/elixir/src/language_servers/next_ls.rs @@ -0,0 +1,176 @@ +use std::fs; + +use zed::lsp::{Completion, CompletionKind, Symbol, SymbolKind}; +use zed::{CodeLabel, CodeLabelSpan, LanguageServerId}; +use zed_extension_api::{self as zed, Result}; + +pub struct NextLs { + cached_binary_path: Option, +} + +impl NextLs { + pub const LANGUAGE_SERVER_ID: &'static str = "next-ls"; + + pub fn new() -> Self { + Self { + cached_binary_path: None, + } + } + + pub fn language_server_binary_path( + &mut self, + language_server_id: &LanguageServerId, + _worktree: &zed::Worktree, + ) -> Result { + if let Some(path) = &self.cached_binary_path { + if fs::metadata(path).map_or(false, |stat| stat.is_file()) { + return Ok(path.clone()); + } + } + + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let release = zed::latest_github_release( + "elixir-tools/next-ls", + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let (platform, arch) = zed::current_platform(); + let asset_name = format!( + "next_ls_{os}_{arch}{extension}", + os = match platform { + zed::Os::Mac => "darwin", + zed::Os::Linux => "linux", + zed::Os::Windows => "windows", + }, + arch = match arch { + zed::Architecture::Aarch64 => "arm64", + zed::Architecture::X8664 => "amd64", + zed::Architecture::X86 => + return Err(format!("unsupported architecture: {arch:?}")), + }, + extension = match platform { + zed::Os::Mac | zed::Os::Linux => "", + zed::Os::Windows => ".exe", + } + ); + + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; + + let version_dir = format!("next-ls-{}", release.version); + fs::create_dir_all(&version_dir).map_err(|e| format!("failed to create directory: {e}"))?; + + let binary_path = format!("{version_dir}/next-ls"); + + if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + + zed::download_file( + &asset.download_url, + &binary_path, + zed::DownloadedFileType::Uncompressed, + ) + .map_err(|e| format!("failed to download file: {e}"))?; + + zed::make_file_executable(&binary_path)?; + + let entries = + fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; + for entry in entries { + let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; + if entry.file_name().to_str() != Some(&version_dir) { + fs::remove_dir_all(&entry.path()).ok(); + } + } + } + + self.cached_binary_path = Some(binary_path.clone()); + Ok(binary_path) + } + + pub fn label_for_completion(&self, completion: Completion) -> Option { + match completion.kind? { + CompletionKind::Module + | CompletionKind::Class + | CompletionKind::Interface + | CompletionKind::Struct => { + let name = completion.label; + let defmodule = "defmodule "; + let code = format!("{defmodule}{name}"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range( + defmodule.len()..defmodule.len() + name.len(), + )], + filter_range: (0..name.len()).into(), + }) + } + CompletionKind::Function | CompletionKind::Constant => { + let name = completion.label; + let def = "def "; + let code = format!("{def}{name}"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range(def.len()..def.len() + name.len())], + filter_range: (0..name.len()).into(), + }) + } + CompletionKind::Operator => { + let name = completion.label; + let def_a = "def a "; + let code = format!("{def_a}{name} b"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range( + def_a.len()..def_a.len() + name.len(), + )], + filter_range: (0..name.len()).into(), + }) + } + _ => None, + } + } + + pub fn label_for_symbol(&self, symbol: Symbol) -> Option { + let name = &symbol.name; + + let (code, filter_range, display_range) = match symbol.kind { + SymbolKind::Module | SymbolKind::Class | SymbolKind::Interface | SymbolKind::Struct => { + let defmodule = "defmodule "; + let code = format!("{defmodule}{name}"); + let filter_range = 0..name.len(); + let display_range = defmodule.len()..defmodule.len() + name.len(); + (code, filter_range, display_range) + } + SymbolKind::Function | SymbolKind::Constant => { + let def = "def "; + let code = format!("{def}{name}"); + let filter_range = 0..name.len(); + let display_range = def.len()..def.len() + name.len(); + (code, filter_range, display_range) + } + _ => return None, + }; + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range(display_range)], + filter_range: filter_range.into(), + code, + }) + } +}