Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
9 changes: 9 additions & 0 deletions crates/lsp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,16 @@ pub enum LanguageId {
JavaScriptReact,
C,
Cpp,
Elixir,
}

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 +58,7 @@ 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" | "eex" | "heex" | "leex" | "neex" => Some(Self::Elixir),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] Mapping .eex/.heex/.leex/.neex to LanguageId::Elixir makes didOpen send languageId elixir for template files. Expert/editor configs treat EEx/HEEx as distinct filetypes (eelixir/heex), so template buffers can be parsed as plain Elixir; add distinct language IDs or otherwise send the template-specific identifiers.

_ => None,
}
}
Expand All @@ -69,6 +76,7 @@ impl LanguageId {
LanguageId::JavaScriptReact => "javascriptreact",
LanguageId::C => "c",
LanguageId::Cpp => "cpp",
LanguageId::Elixir => "elixir",
}
}

Expand All @@ -83,6 +91,7 @@ impl LanguageId {
| LanguageId::JavaScript
| LanguageId::JavaScriptReact => LSPServerType::TypeScriptLanguageServer,
LanguageId::C | LanguageId::Cpp => LSPServerType::Clangd,
LanguageId::Elixir => LSPServerType::Expert,
}
}
}
Expand Down
43 changes: 42 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,42 @@ 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", "eex", "heex", "leex", "neex"] {
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_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_server_type() {
assert_eq!(LanguageId::Elixir.server_type(), LSPServerType::Expert);
}

#[test]
fn test_expert_binary_name() {
assert_eq!(LSPServerType::Expert.binary_name(), "start_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("start_expert")
.arg("--help")
.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 from https://github.com/expert-lsp/expert/releases and place `start_expert` 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
11 changes: 10 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 => "start_expert",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] The public Expert releases install a binary named expert or a platform-specific expert_*, while start_expert is only present in source-built plain releases. Since detection and spawning use binary_name(), users following the linked releases install path will be reported as not installed and unable to start Expert; support the release binary or both names before enabling this.

}
}

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,7 @@ impl LSPServerType {
]
}
LSPServerType::Clangd => vec![LanguageId::C, LanguageId::Cpp],
LSPServerType::Expert => vec![LanguageId::Elixir],
}
}

Expand Down Expand Up @@ -205,6 +213,7 @@ impl LSPServerType {
Box::new(TypeScriptLanguageServerCandidate::new(client))
}
LSPServerType::Clangd => Box::new(ClangdCandidate::new(client)),
LSPServerType::Expert => Box::new(ExpertCandidate::new(client)),
}
}

Expand Down