elixir.rs

  1use anyhow::{anyhow, Context, Result};
  2use async_trait::async_trait;
  3use futures::StreamExt;
  4pub use language::*;
  5use lsp::{CompletionItemKind, SymbolKind};
  6use smol::fs::{self, File};
  7use std::{any::Any, path::PathBuf, sync::Arc};
  8use util::fs::remove_matching;
  9use util::github::latest_github_release;
 10use util::http::HttpClient;
 11use util::ResultExt;
 12
 13use super::github::GitHubLspBinaryVersion;
 14
 15pub struct ElixirLspAdapter;
 16
 17#[async_trait]
 18impl LspAdapter for ElixirLspAdapter {
 19    async fn name(&self) -> LanguageServerName {
 20        LanguageServerName("elixir-ls".into())
 21    }
 22
 23    async fn fetch_latest_server_version(
 24        &self,
 25        http: Arc<dyn HttpClient>,
 26    ) -> Result<Box<dyn 'static + Send + Any>> {
 27        let release = latest_github_release("elixir-lsp/elixir-ls", http).await?;
 28        let asset_name = "elixir-ls.zip";
 29        let asset = release
 30            .assets
 31            .iter()
 32            .find(|asset| asset.name == asset_name)
 33            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
 34        let version = GitHubLspBinaryVersion {
 35            name: release.name,
 36            url: asset.browser_download_url.clone(),
 37        };
 38        Ok(Box::new(version) as Box<_>)
 39    }
 40
 41    async fn fetch_server_binary(
 42        &self,
 43        version: Box<dyn 'static + Send + Any>,
 44        http: Arc<dyn HttpClient>,
 45        container_dir: PathBuf,
 46    ) -> Result<LanguageServerBinary> {
 47        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
 48        let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
 49        let version_dir = container_dir.join(format!("elixir-ls_{}", version.name));
 50        let binary_path = version_dir.join("language_server.sh");
 51
 52        if fs::metadata(&binary_path).await.is_err() {
 53            let mut response = http
 54                .get(&version.url, Default::default(), true)
 55                .await
 56                .context("error downloading release")?;
 57            let mut file = File::create(&zip_path)
 58                .await
 59                .with_context(|| format!("failed to create file {}", zip_path.display()))?;
 60            if !response.status().is_success() {
 61                Err(anyhow!(
 62                    "download failed with status {}",
 63                    response.status().to_string()
 64                ))?;
 65            }
 66            futures::io::copy(response.body_mut(), &mut file).await?;
 67
 68            fs::create_dir_all(&version_dir)
 69                .await
 70                .with_context(|| format!("failed to create directory {}", version_dir.display()))?;
 71            let unzip_status = smol::process::Command::new("unzip")
 72                .arg(&zip_path)
 73                .arg("-d")
 74                .arg(&version_dir)
 75                .output()
 76                .await?
 77                .status;
 78            if !unzip_status.success() {
 79                Err(anyhow!("failed to unzip clangd archive"))?;
 80            }
 81
 82            remove_matching(&container_dir, |entry| entry != version_dir).await;
 83        }
 84
 85        Ok(LanguageServerBinary {
 86            path: binary_path,
 87            arguments: vec![],
 88        })
 89    }
 90
 91    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
 92        (|| async move {
 93            let mut last = None;
 94            let mut entries = fs::read_dir(&container_dir).await?;
 95            while let Some(entry) = entries.next().await {
 96                last = Some(entry?.path());
 97            }
 98            last.map(|path| LanguageServerBinary {
 99                path,
100                arguments: vec![],
101            })
102            .ok_or_else(|| anyhow!("no cached binary"))
103        })()
104        .await
105        .log_err()
106    }
107
108    async fn label_for_completion(
109        &self,
110        completion: &lsp::CompletionItem,
111        language: &Arc<Language>,
112    ) -> Option<CodeLabel> {
113        match completion.kind.zip(completion.detail.as_ref()) {
114            Some((_, detail)) if detail.starts_with("(function)") => {
115                let text = detail.strip_prefix("(function) ")?;
116                let filter_range = 0..text.find('(').unwrap_or(text.len());
117                let source = Rope::from(format!("def {text}").as_str());
118                let runs = language.highlight_text(&source, 4..4 + text.len());
119                return Some(CodeLabel {
120                    text: text.to_string(),
121                    runs,
122                    filter_range,
123                });
124            }
125            Some((_, detail)) if detail.starts_with("(macro)") => {
126                let text = detail.strip_prefix("(macro) ")?;
127                let filter_range = 0..text.find('(').unwrap_or(text.len());
128                let source = Rope::from(format!("defmacro {text}").as_str());
129                let runs = language.highlight_text(&source, 9..9 + text.len());
130                return Some(CodeLabel {
131                    text: text.to_string(),
132                    runs,
133                    filter_range,
134                });
135            }
136            Some((
137                CompletionItemKind::CLASS
138                | CompletionItemKind::MODULE
139                | CompletionItemKind::INTERFACE
140                | CompletionItemKind::STRUCT,
141                _,
142            )) => {
143                let filter_range = 0..completion
144                    .label
145                    .find(" (")
146                    .unwrap_or(completion.label.len());
147                let text = &completion.label[filter_range.clone()];
148                let source = Rope::from(format!("defmodule {text}").as_str());
149                let runs = language.highlight_text(&source, 10..10 + text.len());
150                return Some(CodeLabel {
151                    text: completion.label.clone(),
152                    runs,
153                    filter_range,
154                });
155            }
156            _ => {}
157        }
158
159        None
160    }
161
162    async fn label_for_symbol(
163        &self,
164        name: &str,
165        kind: SymbolKind,
166        language: &Arc<Language>,
167    ) -> Option<CodeLabel> {
168        let (text, filter_range, display_range) = match kind {
169            SymbolKind::METHOD | SymbolKind::FUNCTION => {
170                let text = format!("def {}", name);
171                let filter_range = 4..4 + name.len();
172                let display_range = 0..filter_range.end;
173                (text, filter_range, display_range)
174            }
175            SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => {
176                let text = format!("defmodule {}", name);
177                let filter_range = 10..10 + name.len();
178                let display_range = 0..filter_range.end;
179                (text, filter_range, display_range)
180            }
181            _ => return None,
182        };
183
184        Some(CodeLabel {
185            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
186            text: text[display_range].to_string(),
187            filter_range,
188        })
189    }
190}