Detailed changes
@@ -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"
@@ -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" }
@@ -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
@@ -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"]),
@@ -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
@@ -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<String>,
- },
-}
-
-#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
-pub struct ElixirSettingsContent {
- lsp: Option<ElixirLspSetting>,
-}
-
-impl Settings for ElixirSettings {
- const KEY: Option<&'static str> = Some("elixir");
-
- type FileContent = ElixirSettingsContent;
-
- fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
- 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<dyn LspAdapterDelegate>,
- cx: &mut AsyncAppContext,
- ) -> Option<Task<Result<()>>> {
- 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<Box<dyn 'static + Send + Any>> {
- 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<dyn 'static + Send + Any>,
- container_dir: PathBuf,
- delegate: &dyn LspAdapterDelegate,
- ) -> Result<LanguageServerBinary> {
- let version = version.downcast::<GitHubLspBinaryVersion>().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<LanguageServerBinary> {
- get_cached_server_binary_elixir_ls(container_dir).await
- }
-
- async fn installation_test_binary(
- &self,
- container_dir: PathBuf,
- ) -> Option<LanguageServerBinary> {
- get_cached_server_binary_elixir_ls(container_dir).await
- }
-
- async fn label_for_completion(
- &self,
- completion: &lsp::CompletionItem,
- language: &Arc<Language>,
- ) -> Option<CodeLabel> {
- 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<Language>,
- ) -> Option<CodeLabel> {
- 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<Self>,
- _: &Arc<dyn LspAdapterDelegate>,
- cx: &mut AsyncAppContext,
- ) -> Result<Value> {
- 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<LanguageServerBinary> {
- 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<Box<dyn 'static + Send + Any>> {
- 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<dyn 'static + Send + Any>,
- container_dir: PathBuf,
- delegate: &dyn LspAdapterDelegate,
- ) -> Result<LanguageServerBinary> {
- let version = version.downcast::<GitHubLspBinaryVersion>().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,
- <fs::Permissions as fs::unix::PermissionsExt>::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<LanguageServerBinary> {
- 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<LanguageServerBinary> {
- 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<Language>,
- ) -> Option<CodeLabel> {
- label_for_completion_elixir(completion, language)
- }
-
- async fn label_for_symbol(
- &self,
- name: &str,
- symbol_kind: SymbolKind,
- language: &Arc<Language>,
- ) -> Option<CodeLabel> {
- label_for_symbol_elixir(name, symbol_kind, language)
- }
-}
-
-async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option<LanguageServerBinary> {
- 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<String>,
-}
-
-#[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<Box<dyn 'static + Send + Any>> {
- Ok(Box::new(()) as Box<_>)
- }
-
- async fn fetch_server_binary(
- &self,
- _: Box<dyn 'static + Send + Any>,
- _: PathBuf,
- _: &dyn LspAdapterDelegate,
- ) -> Result<LanguageServerBinary> {
- 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<LanguageServerBinary> {
- 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<LanguageServerBinary> {
- 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<Language>,
- ) -> Option<CodeLabel> {
- label_for_completion_elixir(completion, language)
- }
-
- async fn label_for_symbol(
- &self,
- name: &str,
- symbol: SymbolKind,
- language: &Arc<Language>,
- ) -> Option<CodeLabel> {
- label_for_symbol_elixir(name, symbol, language)
- }
-}
-
-fn label_for_completion_elixir(
- completion: &lsp::CompletionItem,
- language: &Arc<Language>,
-) -> Option<CodeLabel> {
- 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<Language>,
-) -> Option<CodeLabel> {
- 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()
- },
- ]))
-}
@@ -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<dyn NodeRuntime>,
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",
@@ -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"
@@ -0,0 +1 @@
+../../LICENSE-APACHE
@@ -0,0 +1,27 @@
+id = "elixir"
+name = "Elixir"
+description = "Elixir support."
+version = "0.0.1"
+schema_version = 1
+authors = ["Marshall Bowers <elliott.codes@gmail.com>"]
+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"
@@ -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"]
+ }
+]
@@ -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<ElixirLs>,
+ next_ls: Option<NextLs>,
+ lexical: Option<Lexical>,
+}
+
+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<zed::Command> {
+ 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<CodeLabel> {
+ 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<CodeLabel> {
+ 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<Option<serde_json::Value>> {
+ 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);
@@ -0,0 +1,7 @@
+mod elixir_ls;
+mod lexical;
+mod next_ls;
+
+pub use elixir_ls::*;
+pub use lexical::*;
+pub use next_ls::*;
@@ -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<String>,
+}
+
+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<String> {
+ 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<CodeLabel> {
+ 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<CodeLabel> {
+ 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,
+ })
+ }
+}
@@ -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<String>,
+}
+
+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<String> {
+ 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<CodeLabel> {
+ 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<CodeLabel> {
+ None
+ }
+}
@@ -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<String>,
+}
+
+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<String> {
+ 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<CodeLabel> {
+ 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<CodeLabel> {
+ 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,
+ })
+ }
+}