Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions crates/lsp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,18 @@ pub enum LanguageId {
JavaScriptReact,
C,
Cpp,
Elixir,
Eex,
PhoenixHeex,
}

impl LanguageId {
pub fn from_path(path: &Path) -> Option<Self> {
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),
Expand All @@ -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,
}
}
Expand All @@ -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",
}
}

Expand All @@ -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
}
}
}
}
Expand Down
76 changes: 75 additions & 1 deletion crates/lsp/src/config_tests.rs
Original file line number Diff line number Diff line change
@@ -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))]
Expand Down Expand Up @@ -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");
}
87 changes: 87 additions & 0 deletions crates/lsp/src/servers/expert.rs
Original file line number Diff line number Diff line change
@@ -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<http_client::Client>,
}

impl ExpertCandidate {
pub fn new(client: Arc<http_client::Client>) -> 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<LanguageServerMetadata> {
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<LanguageServerMetadata> {
todo!()
}
}
1 change: 1 addition & 0 deletions crates/lsp/src/servers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod clangd;
pub mod expert;
pub mod go;
pub mod pyright;
pub mod rust;
Expand Down
16 changes: 15 additions & 1 deletion crates/lsp/src/supported_servers.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -42,6 +43,7 @@ pub enum LSPServerType {
Pyright,
TypeScriptLanguageServer,
Clangd,
Expert,
}

/// Provides server-specific configuration for each LSP server type.
Expand Down Expand Up @@ -109,6 +111,7 @@ impl LSPServerType {
binary_path: path,
prepend_args: vec![],
}),
LSPServerType::Expert => None,
}
}

Expand All @@ -132,6 +135,7 @@ impl LSPServerType {
LSPServerType::Pyright => "pyright-langserver",
LSPServerType::TypeScriptLanguageServer => "typescript-language-server",
LSPServerType::Clangd => "clangd",
LSPServerType::Expert => "expert",
}
}

Expand All @@ -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"],
}
}

Expand All @@ -154,6 +160,7 @@ impl LSPServerType {
LSPServerType::Pyright => vec!["--stdio"],
LSPServerType::TypeScriptLanguageServer => vec!["--stdio"],
LSPServerType::Clangd => vec![],
LSPServerType::Expert => vec!["--stdio"],
}
}

Expand All @@ -172,6 +179,11 @@ impl LSPServerType {
]
}
LSPServerType::Clangd => vec![LanguageId::C, LanguageId::Cpp],
LSPServerType::Expert => vec![
LanguageId::Elixir,
LanguageId::Eex,
LanguageId::PhoenixHeex,
],
}
}

Expand All @@ -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()
Expand All @@ -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)),
}
}

Expand Down