elixir.rs

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