Extract Elixir extension (#10948)

Marshall Bowers created

This PR extracts Elixir support into an extension and removes the
built-in Elixir support from Zed.

As part of this, [Lexical](https://github.com/lexical-lsp/lexical) has
been added as an available language server for Elixir.

Since the Elixir extension provides three different language servers,
you'll need to use the `language_servers` setting to select the one you
want to use:

#### Elixir LS

```json
{
  "languages": {
    "Elixir": {
      "language_servers": [ "elixir-ls", "!next-ls", "!lexical", "..."]
    }
  }
}
```

#### Next LS

```json
{
  "languages": {
    "Elixir": {
      "language_servers": [ "next-ls", "!elixir-ls", "!lexical", "..."]
    }
  }
}
```

#### Lexical

```json
{
  "languages": {
    "Elixir": {
      "language_servers": [ "lexical", "!elixir-ls", "!next-ls", "..."]
    }
  }
}
```

These can either go in your user settings or your project settings.

Release Notes:

- Removed built-in support for Elixir, in favor of making it available
as an extension.

Change summary

Cargo.lock                                          |  14 
Cargo.toml                                          |   3 
assets/settings/default.json                        |  21 
crates/extensions_ui/src/extension_suggest.rs       |   1 
crates/languages/Cargo.toml                         |   5 
crates/languages/src/elixir.rs                      | 607 ---------------
crates/languages/src/lib.rs                         |  52 -
extensions/elixir/Cargo.toml                        |  16 
extensions/elixir/LICENSE-APACHE                    |   1 
extensions/elixir/extension.toml                    |  27 
extensions/elixir/languages/elixir/brackets.scm     |   0 
extensions/elixir/languages/elixir/config.toml      |   0 
extensions/elixir/languages/elixir/embedding.scm    |   0 
extensions/elixir/languages/elixir/highlights.scm   |   0 
extensions/elixir/languages/elixir/indents.scm      |   0 
extensions/elixir/languages/elixir/injections.scm   |   0 
extensions/elixir/languages/elixir/outline.scm      |   0 
extensions/elixir/languages/elixir/overrides.scm    |   0 
extensions/elixir/languages/elixir/tasks.json       |  28 
extensions/elixir/languages/heex/config.toml        |   0 
extensions/elixir/languages/heex/highlights.scm     |   0 
extensions/elixir/languages/heex/injections.scm     |   0 
extensions/elixir/languages/heex/overrides.scm      |   0 
extensions/elixir/src/elixir.rs                     | 107 ++
extensions/elixir/src/language_servers.rs           |   7 
extensions/elixir/src/language_servers/elixir_ls.rs | 165 ++++
extensions/elixir/src/language_servers/lexical.rs   | 130 +++
extensions/elixir/src/language_servers/next_ls.rs   | 176 ++++
28 files changed, 671 insertions(+), 689 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -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"

Cargo.toml 🔗

@@ -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" }
 

assets/settings/default.json 🔗

@@ -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

crates/extensions_ui/src/extension_suggest.rs 🔗

@@ -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"]),

crates/languages/Cargo.toml 🔗

@@ -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

crates/languages/src/elixir.rs 🔗

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

crates/languages/src/lib.rs 🔗

@@ -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",

extensions/elixir/Cargo.toml 🔗

@@ -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"

extensions/elixir/extension.toml 🔗

@@ -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"

extensions/elixir/languages/elixir/tasks.json 🔗

@@ -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"]
+  }
+]

extensions/elixir/src/elixir.rs 🔗

@@ -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);

extensions/elixir/src/language_servers/elixir_ls.rs 🔗

@@ -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,
+        })
+    }
+}

extensions/elixir/src/language_servers/lexical.rs 🔗

@@ -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
+    }
+}

extensions/elixir/src/language_servers/next_ls.rs 🔗

@@ -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,
+        })
+    }
+}