diff --git a/crates/lsp/src/config.rs b/crates/lsp/src/config.rs index 71cb9cac0..d4c999ba9 100644 --- a/crates/lsp/src/config.rs +++ b/crates/lsp/src/config.rs @@ -32,10 +32,18 @@ pub enum LanguageId { JavaScriptReact, C, Cpp, + Elixir, + Eex, + PhoenixHeex, } impl LanguageId { pub fn from_path(path: &Path) -> Option { + if let Some("mix.exs" | "mix.lock" | ".formatter.exs") = + path.file_name().and_then(|n| n.to_str()) + { + return Some(Self::Elixir); + } let extn = path.extension()?; match extn.to_str()? { "rs" => Some(Self::Rust), @@ -52,6 +60,9 @@ impl LanguageId { // compile_commands.json is present, clangd will use the correct language // regardless of the languageId we send. "h" | "H" | "hh" | "hpp" | "hxx" => Some(Self::Cpp), + "ex" | "exs" => Some(Self::Elixir), + "eex" | "leex" => Some(Self::Eex), + "heex" => Some(Self::PhoenixHeex), _ => None, } } @@ -69,6 +80,9 @@ impl LanguageId { LanguageId::JavaScriptReact => "javascriptreact", LanguageId::C => "c", LanguageId::Cpp => "cpp", + LanguageId::Elixir => "elixir", + LanguageId::Eex => "eex", + LanguageId::PhoenixHeex => "phoenix-heex", } } @@ -83,6 +97,9 @@ impl LanguageId { | LanguageId::JavaScript | LanguageId::JavaScriptReact => LSPServerType::TypeScriptLanguageServer, LanguageId::C | LanguageId::Cpp => LSPServerType::Clangd, + LanguageId::Elixir | LanguageId::Eex | LanguageId::PhoenixHeex => { + LSPServerType::Expert + } } } } diff --git a/crates/lsp/src/config_tests.rs b/crates/lsp/src/config_tests.rs index e4ec2dd22..de0224455 100644 --- a/crates/lsp/src/config_tests.rs +++ b/crates/lsp/src/config_tests.rs @@ -1,8 +1,10 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use lsp_types::Uri; use crate::config::{lsp_uri_to_path, path_to_lsp_uri}; +use crate::supported_servers::LSPServerType; +use crate::LanguageId; // Unix-specific tests use Unix paths #[cfg(not(windows))] @@ -216,3 +218,75 @@ fn test_path_to_lsp_uri_rejects_relative_path() { assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("must be absolute")); } + +#[test] +fn test_elixir_language_id_from_extension() { + for ext in ["ex", "exs"] { + let path = PathBuf::from(format!("foo.{ext}")); + assert_eq!( + LanguageId::from_path(&path), + Some(LanguageId::Elixir), + "extension .{ext} should map to Elixir" + ); + } +} + +#[test] +fn test_eex_language_id_from_extension() { + for ext in ["eex", "leex"] { + let path = PathBuf::from(format!("foo.{ext}")); + assert_eq!( + LanguageId::from_path(&path), + Some(LanguageId::Eex), + "extension .{ext} should map to Eex" + ); + } +} + +#[test] +fn test_phoenix_heex_language_id_from_extension() { + let path = PathBuf::from("foo.heex"); + assert_eq!(LanguageId::from_path(&path), Some(LanguageId::PhoenixHeex)); +} + +#[test] +fn test_elixir_language_id_from_filename() { + for name in ["mix.exs", "mix.lock", ".formatter.exs"] { + let path = Path::new(name); + assert_eq!( + LanguageId::from_path(path), + Some(LanguageId::Elixir), + "filename {name} should map to Elixir" + ); + } +} + +#[test] +fn test_elixir_family_server_type() { + assert_eq!(LanguageId::Elixir.server_type(), LSPServerType::Expert); + assert_eq!(LanguageId::Eex.server_type(), LSPServerType::Expert); + assert_eq!( + LanguageId::PhoenixHeex.server_type(), + LSPServerType::Expert + ); +} + +#[test] +fn test_elixir_family_lsp_identifiers() { + assert_eq!(LanguageId::Elixir.lsp_language_identifier(), "elixir"); + assert_eq!(LanguageId::Eex.lsp_language_identifier(), "eex"); + assert_eq!( + LanguageId::PhoenixHeex.lsp_language_identifier(), + "phoenix-heex" + ); +} + +#[test] +fn test_expert_binary_name() { + assert_eq!(LSPServerType::Expert.binary_name(), "expert"); +} + +#[test] +fn test_expert_language_name() { + assert_eq!(LSPServerType::Expert.language_name(), "Elixir"); +} diff --git a/crates/lsp/src/servers/expert.rs b/crates/lsp/src/servers/expert.rs new file mode 100644 index 000000000..5693aedb7 --- /dev/null +++ b/crates/lsp/src/servers/expert.rs @@ -0,0 +1,87 @@ +use std::path::Path; +use std::sync::Arc; + +use crate::language_server_candidate::{LanguageServerCandidate, LanguageServerMetadata}; +use crate::CommandBuilder; +use async_trait::async_trait; + +#[cfg_attr(not(feature = "local_fs"), allow(dead_code))] +pub struct ExpertCandidate { + #[allow(dead_code)] + client: Arc, +} + +impl ExpertCandidate { + pub fn new(client: Arc) -> Self { + Self { client } + } +} + +#[async_trait] +#[cfg(feature = "local_fs")] +impl LanguageServerCandidate for ExpertCandidate { + async fn should_suggest_for_repo(&self, path: &Path, executor: &CommandBuilder) -> bool { + (path.join("mix.exs").exists() + || path.join("mix.lock").exists() + || path.join(".formatter.exs").exists()) + && self.is_installed_on_path(executor).await + } + + async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool { + false + } + + async fn is_installed_on_path(&self, executor: &CommandBuilder) -> bool { + executor + .command("expert") + .arg("--version") + .output() + .await + .map(|o| o.status.success()) + .unwrap_or(false) + } + + async fn install( + &self, + _metadata: LanguageServerMetadata, + _executor: &CommandBuilder, + ) -> anyhow::Result<()> { + anyhow::bail!( + "Install Expert manually: download the binary for your platform (e.g. `expert_darwin_arm64`) from https://github.com/expert-lsp/expert/releases, rename it to `expert`, and place it on your PATH" + ) + } + + async fn fetch_latest_server_metadata(&self) -> anyhow::Result { + anyhow::bail!( + "Auto-install not supported; download from https://github.com/expert-lsp/expert/releases" + ) + } +} + +#[async_trait] +#[cfg(not(feature = "local_fs"))] +impl LanguageServerCandidate for ExpertCandidate { + async fn should_suggest_for_repo(&self, _path: &Path, _executor: &CommandBuilder) -> bool { + false + } + + async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool { + false + } + + async fn is_installed_on_path(&self, _executor: &CommandBuilder) -> bool { + false + } + + async fn install( + &self, + _metadata: LanguageServerMetadata, + _executor: &CommandBuilder, + ) -> anyhow::Result<()> { + todo!() + } + + async fn fetch_latest_server_metadata(&self) -> anyhow::Result { + todo!() + } +} diff --git a/crates/lsp/src/servers/mod.rs b/crates/lsp/src/servers/mod.rs index 32d836768..7048e257a 100644 --- a/crates/lsp/src/servers/mod.rs +++ b/crates/lsp/src/servers/mod.rs @@ -1,4 +1,5 @@ pub mod clangd; +pub mod expert; pub mod go; pub mod pyright; pub mod rust; diff --git a/crates/lsp/src/supported_servers.rs b/crates/lsp/src/supported_servers.rs index 80ba19253..5a221327d 100644 --- a/crates/lsp/src/supported_servers.rs +++ b/crates/lsp/src/supported_servers.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use crate::servers::clangd::ClangdCandidate; +use crate::servers::expert::ExpertCandidate; use crate::servers::go::GoPlsCandidate; use crate::servers::pyright::PyrightCandidate; use crate::servers::rust::RustAnalyzerCandidate; @@ -42,6 +43,7 @@ pub enum LSPServerType { Pyright, TypeScriptLanguageServer, Clangd, + Expert, } /// Provides server-specific configuration for each LSP server type. @@ -109,6 +111,7 @@ impl LSPServerType { binary_path: path, prepend_args: vec![], }), + LSPServerType::Expert => None, } } @@ -132,6 +135,7 @@ impl LSPServerType { LSPServerType::Pyright => "pyright-langserver", LSPServerType::TypeScriptLanguageServer => "typescript-language-server", LSPServerType::Clangd => "clangd", + LSPServerType::Expert => "expert", } } @@ -140,7 +144,9 @@ impl LSPServerType { fn args(&self) -> Vec<&'static str> { match self { LSPServerType::RustAnalyzer | LSPServerType::GoPls | LSPServerType::Clangd => vec![], - LSPServerType::Pyright | LSPServerType::TypeScriptLanguageServer => vec!["--stdio"], + LSPServerType::Pyright + | LSPServerType::TypeScriptLanguageServer + | LSPServerType::Expert => vec!["--stdio"], } } @@ -154,6 +160,7 @@ impl LSPServerType { LSPServerType::Pyright => vec!["--stdio"], LSPServerType::TypeScriptLanguageServer => vec!["--stdio"], LSPServerType::Clangd => vec![], + LSPServerType::Expert => vec!["--stdio"], } } @@ -172,6 +179,11 @@ impl LSPServerType { ] } LSPServerType::Clangd => vec![LanguageId::C, LanguageId::Cpp], + LSPServerType::Expert => vec![ + LanguageId::Elixir, + LanguageId::Eex, + LanguageId::PhoenixHeex, + ], } } @@ -180,6 +192,7 @@ impl LSPServerType { pub fn language_name(&self) -> String { match self { LSPServerType::TypeScriptLanguageServer => "TypeScript/JavaScript".to_string(), + LSPServerType::Expert => "Elixir".to_string(), _ => self .languages() .iter() @@ -205,6 +218,7 @@ impl LSPServerType { Box::new(TypeScriptLanguageServerCandidate::new(client)) } LSPServerType::Clangd => Box::new(ClangdCandidate::new(client)), + LSPServerType::Expert => Box::new(ExpertCandidate::new(client)), } }