feat(tools): Install ElixirLS and its debugger, checking for Elixir first

ElixirLS ships one platform-independent zip of launcher scripts that
build the server with the project's Elixir / OTP on first start. Both
the language server and the debug adapter are installed from it, each
linked to its own launcher (language_server / debug_adapter).

Tools can now declare runtimes they need on PATH; Install stays
disabled with the reason ("Elixir is required") until they are found.

The Elixir debug adapter is renamed elixir-ls-debugger so it no longer
shares the server's tool name.

Claude-Session: https://claude.ai/code/session_01B65qLEKnkeYTGdbwhLpjCu
This commit is contained in:
l0ng-ai
2026-10-24 11:26:31 +08:00
parent 03a660354c
commit 135723bca6
4 changed files with 94 additions and 6 deletions
+8 -5
View File
@@ -222,11 +222,8 @@ fn usages(langs: &[LanguageConfig]) -> BTreeMap<String, Usage> {
if let Some(d) = &l.debugger {
// An adapter named like a server but launched differently (ElixirLS's
// `elixir-ls-debugger`) is a tool of its own, listed by its command.
let name = if servers.contains(d.adapter.as_str()) && !TOOLS.iter().any(|t| t.name == d.adapter) {
&d.command
} else {
&d.adapter
};
let name =
if servers.contains(d.adapter.as_str()) && d.command != d.adapter { &d.command } else { &d.adapter };
add(name, &d.command, ToolKind::Debugger, d.install_hint.as_ref(), &l.name);
}
}
@@ -365,6 +362,12 @@ impl Tools {
/// Requirement check for installing `spec` right now.
fn unavailable_reason(&self, spec: &ToolSpec) -> Option<String> {
self.installer_missing(spec)
.or_else(|| spec.needs.iter().find(|(cmd, _)| self.which(cmd).is_none()).map(|(_, why)| why.to_string()))
}
/// What the install method itself lacks (npm, a build for this platform, …).
fn installer_missing(&self, spec: &ToolSpec) -> Option<String> {
let need = |ok: bool, msg: &str| (!ok).then(|| msg.to_string());
match spec.method {
Method::Rustup { .. } => need(self.which("rustup").is_some(), "rustup is required (https://rustup.rs)"),
+66
View File
@@ -78,6 +78,8 @@ pub struct ToolSpec {
/// Version query: a command (looked up next to the main binary first) and its args.
pub version: Option<(&'static str, &'static [&'static str])>,
pub homepage: &'static str,
/// Runtimes the tool runs on, found on `PATH`: command and why it is needed.
pub needs: &'static [(&'static str, &'static str)],
}
impl ToolSpec {
@@ -94,6 +96,9 @@ use Os::{Linux, Mac, Windows};
use ToolKind::{Debugger, LanguageServer, Plugin};
const V: Option<(&str, &[&str])> = None;
const ELIXIR: &[(&str, &str)] = &[("elixir", "Elixir is required (https://elixir-lang.org/install.html)")];
const ELIXIR_LS_ASSETS: &[AssetRule] =
&[any_arch(Mac, "elixir-ls-v*.zip"), any_arch(Linux, "elixir-ls-v*.zip"), any_arch(Windows, "elixir-ls-v*.zip")];
const fn ver(cmd: &'static str) -> Option<(&'static str, &'static [&'static str])> {
Some((cmd, &["--version"]))
}
@@ -107,6 +112,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Rustup { component: "rust-analyzer" },
version: ver("rust-analyzer"),
homepage: "https://rust-analyzer.github.io",
needs: &[],
},
ToolSpec {
name: "vtsls",
@@ -116,6 +122,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Npm { packages: &["@vtsls/language-server"] },
version: V,
homepage: "https://github.com/yioneko/vtsls",
needs: &[],
},
ToolSpec {
name: "typescript-language-server",
@@ -125,6 +132,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Npm { packages: &["typescript-language-server", "typescript"] },
version: ver("typescript-language-server"),
homepage: "https://github.com/typescript-language-server/typescript-language-server",
needs: &[],
},
ToolSpec {
name: "pyright",
@@ -134,6 +142,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Npm { packages: &["pyright"] },
version: ver("pyright"),
homepage: "https://github.com/microsoft/pyright",
needs: &[],
},
ToolSpec {
name: "basedpyright",
@@ -143,6 +152,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::PipVenv { package: "basedpyright" },
version: ver("basedpyright"),
homepage: "https://docs.basedpyright.com",
needs: &[],
},
ToolSpec {
name: "bash-language-server",
@@ -152,6 +162,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Npm { packages: &["bash-language-server"] },
version: ver("bash-language-server"),
homepage: "https://github.com/bash-lsp/bash-language-server",
needs: &[],
},
ToolSpec {
name: "yaml-language-server",
@@ -161,6 +172,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Npm { packages: &["yaml-language-server"] },
version: V,
homepage: "https://github.com/redhat-developer/yaml-language-server",
needs: &[],
},
ToolSpec {
name: "vscode-json-language-server",
@@ -170,6 +182,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Npm { packages: &["vscode-langservers-extracted"] },
version: V,
homepage: "https://github.com/hrsh7th/vscode-langservers-extracted",
needs: &[],
},
ToolSpec {
name: "vscode-css-language-server",
@@ -179,6 +192,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Npm { packages: &["vscode-langservers-extracted"] },
version: V,
homepage: "https://github.com/hrsh7th/vscode-langservers-extracted",
needs: &[],
},
ToolSpec {
name: "vscode-html-language-server",
@@ -188,6 +202,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Npm { packages: &["vscode-langservers-extracted"] },
version: V,
homepage: "https://github.com/hrsh7th/vscode-langservers-extracted",
needs: &[],
},
ToolSpec {
name: "dockerfile-language-server",
@@ -197,6 +212,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Npm { packages: &["dockerfile-language-server-nodejs"] },
version: V,
homepage: "https://github.com/rcjsuen/dockerfile-language-server",
needs: &[],
},
ToolSpec {
name: "svelte-language-server",
@@ -206,6 +222,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Npm { packages: &["svelte-language-server"] },
version: V,
homepage: "https://github.com/sveltejs/language-tools",
needs: &[],
},
ToolSpec {
name: "vue-language-server",
@@ -215,6 +232,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Npm { packages: &["@vue/language-server"] },
version: V,
homepage: "https://github.com/vuejs/language-tools",
needs: &[],
},
ToolSpec {
name: "graphql-lsp",
@@ -224,6 +242,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Npm { packages: &["graphql-language-service-cli"] },
version: V,
homepage: "https://github.com/graphql/graphiql",
needs: &[],
},
ToolSpec {
name: "intelephense",
@@ -233,6 +252,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Npm { packages: &["intelephense"] },
version: V,
homepage: "https://intelephense.com",
needs: &[],
},
ToolSpec {
name: "gopls",
@@ -242,6 +262,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Go { module: "golang.org/x/tools/gopls@latest" },
version: Some(("gopls", &["version"])),
homepage: "https://pkg.go.dev/golang.org/x/tools/gopls",
needs: &[],
},
ToolSpec {
name: "sqls",
@@ -251,6 +272,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Go { module: "github.com/sqls-server/sqls@latest" },
version: Some(("sqls", &["--version"])),
homepage: "https://github.com/sqls-server/sqls",
needs: &[],
},
ToolSpec {
name: "delve",
@@ -260,6 +282,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::Go { module: "github.com/go-delve/delve/cmd/dlv@latest" },
version: Some(("dlv", &["version"])),
homepage: "https://github.com/go-delve/delve",
needs: &[],
},
ToolSpec {
name: "debugpy",
@@ -269,6 +292,7 @@ pub static TOOLS: &[ToolSpec] = &[
method: Method::PipTarget { package: "debugpy", module: "debugpy" },
version: V,
homepage: "https://github.com/microsoft/debugpy",
needs: &[],
},
ToolSpec {
name: "clangd",
@@ -286,6 +310,7 @@ pub static TOOLS: &[ToolSpec] = &[
},
version: ver("clangd"),
homepage: "https://clangd.llvm.org",
needs: &[],
},
ToolSpec {
name: "lua-language-server",
@@ -305,6 +330,7 @@ pub static TOOLS: &[ToolSpec] = &[
},
version: ver("lua-language-server"),
homepage: "https://luals.github.io",
needs: &[],
},
ToolSpec {
name: "marksman",
@@ -323,6 +349,7 @@ pub static TOOLS: &[ToolSpec] = &[
},
version: ver("marksman"),
homepage: "https://github.com/artempyanykh/marksman",
needs: &[],
},
ToolSpec {
name: "taplo",
@@ -342,6 +369,7 @@ pub static TOOLS: &[ToolSpec] = &[
},
version: ver("taplo"),
homepage: "https://taplo.tamasfe.dev",
needs: &[],
},
ToolSpec {
name: "zls",
@@ -361,6 +389,7 @@ pub static TOOLS: &[ToolSpec] = &[
},
version: ver("zls"),
homepage: "https://github.com/zigtools/zls",
needs: &[],
},
ToolSpec {
name: "terraform-ls",
@@ -380,6 +409,7 @@ pub static TOOLS: &[ToolSpec] = &[
},
version: Some(("terraform-ls", &["version"])),
homepage: "https://github.com/hashicorp/terraform-ls",
needs: &[],
},
ToolSpec {
name: "codelldb",
@@ -404,6 +434,7 @@ pub static TOOLS: &[ToolSpec] = &[
},
version: V,
homepage: "https://github.com/vadimcn/codelldb",
needs: &[],
},
ToolSpec {
name: "js-debug",
@@ -422,6 +453,7 @@ pub static TOOLS: &[ToolSpec] = &[
},
version: V,
homepage: "https://github.com/microsoft/vscode-js-debug",
needs: &[],
},
ToolSpec {
name: "jdtls",
@@ -439,6 +471,7 @@ pub static TOOLS: &[ToolSpec] = &[
},
version: V,
homepage: "https://github.com/eclipse-jdtls/eclipse.jdt.ls",
needs: &[],
},
ToolSpec {
name: "java-debug",
@@ -457,6 +490,7 @@ pub static TOOLS: &[ToolSpec] = &[
},
version: V,
homepage: "https://github.com/microsoft/java-debug",
needs: &[],
},
ToolSpec {
name: "lombok",
@@ -475,6 +509,7 @@ pub static TOOLS: &[ToolSpec] = &[
},
version: V,
homepage: "https://projectlombok.org",
needs: &[],
},
ToolSpec {
name: "kotlin-lsp",
@@ -498,6 +533,37 @@ pub static TOOLS: &[ToolSpec] = &[
},
version: V,
homepage: "https://github.com/Kotlin/kotlin-lsp",
needs: &[],
},
// ElixirLS ships launcher scripts only: on first start they build the server
// with the Elixir / OTP found for the project (Mix.install, needs network).
ToolSpec {
name: "elixir-ls",
display: "ElixirLS",
kind: LanguageServer,
commands: &["elixir-ls"],
method: Method::Release {
source: ReleaseSource::GitHub { repo: "elixir-lsp/elixir-ls" },
assets: ELIXIR_LS_ASSETS,
locate: &["language_server.sh", "language_server.bat"],
},
version: V,
homepage: "https://github.com/elixir-lsp/elixir-ls",
needs: ELIXIR,
},
ToolSpec {
name: "elixir-ls-debugger",
display: "ElixirLS Debugger",
kind: Debugger,
commands: &["elixir-ls-debugger"],
method: Method::Release {
source: ReleaseSource::GitHub { repo: "elixir-lsp/elixir-ls" },
assets: ELIXIR_LS_ASSETS,
locate: &["debug_adapter.sh", "debug_adapter.bat"],
},
version: V,
homepage: "https://github.com/elixir-lsp/elixir-ls",
needs: ELIXIR,
},
];
+19
View File
@@ -317,3 +317,22 @@ fn a_debugger_named_like_its_server_is_listed_by_its_command() {
assert_eq!((debugger.kind, debugger.command.as_str()), (ToolKind::Debugger, "elixir-ls-debugger"));
assert_eq!(debugger.languages, vec!["Elixir".to_string()]);
}
#[cfg(unix)]
#[test]
fn elixir_ls_needs_elixir_and_lists_its_debugger() {
let env = Env::new();
env.script(&env.sys, "curl", "exit 0");
let list = env.tools().list(&langs());
let server = find(&list, "elixir-ls");
assert_eq!(server.install_method.as_deref(), Some("GitHub release"));
assert!(!server.can_install);
assert!(server.unavailable_reason.as_deref().unwrap().contains("Elixir is required"));
let debugger = find(&list, "elixir-ls-debugger");
assert_eq!((debugger.kind, debugger.languages.as_slice()), (ToolKind::Debugger, ["Elixir".to_string()].as_slice()));
env.script(&env.sys, "elixir", "exit 0");
let list = env.tools().list(&langs());
assert!(find(&list, "elixir-ls").can_install);
assert!(find(&list, "elixir-ls-debugger").can_install);
}
+1 -1
View File
@@ -14,6 +14,6 @@ root-markers = ["mix.exs"]
install-hint = "Install ElixirLS (https://github.com/elixir-lsp/elixir-ls/releases) and expose language_server.sh as `elixir-ls`"
[debugger]
adapter = "elixir-ls"
adapter = "elixir-ls-debugger"
command = "elixir-ls-debugger"
install-hint = "Install ElixirLS and expose debug_adapter.sh as `elixir-ls-debugger`"