elixir.rs

  1use anyhow::{anyhow, Context, Result};
  2use async_trait::async_trait;
  3use futures::StreamExt;
  4use gpui::{AsyncAppContext, Task};
  5pub use language::*;
  6use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
  7use smol::fs::{self, File};
  8use std::{
  9    any::Any,
 10    path::PathBuf,
 11    sync::{
 12        atomic::{AtomicBool, Ordering::SeqCst},
 13        Arc,
 14    },
 15};
 16use util::{
 17    fs::remove_matching,
 18    github::{latest_github_release, GitHubLspBinaryVersion},
 19    ResultExt,
 20};
 21
 22pub struct ElixirLspAdapter;
 23
 24#[async_trait]
 25impl LspAdapter for ElixirLspAdapter {
 26    async fn name(&self) -> LanguageServerName {
 27        LanguageServerName("elixir-ls".into())
 28    }
 29
 30    fn short_name(&self) -> &'static str {
 31        "elixir-ls"
 32    }
 33
 34    fn will_start_server(
 35        &self,
 36        delegate: &Arc<dyn LspAdapterDelegate>,
 37        cx: &mut AsyncAppContext,
 38    ) -> Option<Task<Result<()>>> {
 39        static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
 40
 41        const NOTIFICATION_MESSAGE: &str = "Could not run the elixir language server, `elixir-ls`, because `elixir` was not found.";
 42
 43        let delegate = delegate.clone();
 44        Some(cx.spawn(|mut cx| async move {
 45            let elixir_output = smol::process::Command::new("elixir")
 46                .args(["--version"])
 47                .output()
 48                .await;
 49            if elixir_output.is_err() {
 50                if DID_SHOW_NOTIFICATION
 51                    .compare_exchange(false, true, SeqCst, SeqCst)
 52                    .is_ok()
 53                {
 54                    cx.update(|cx| {
 55                        delegate.show_notification(NOTIFICATION_MESSAGE, cx);
 56                    })
 57                }
 58                return Err(anyhow!("cannot run elixir-ls"));
 59            }
 60
 61            Ok(())
 62        }))
 63    }
 64
 65    async fn fetch_latest_server_version(
 66        &self,
 67        delegate: &dyn LspAdapterDelegate,
 68    ) -> Result<Box<dyn 'static + Send + Any>> {
 69        let http = delegate.http_client();
 70        let release = latest_github_release("elixir-lsp/elixir-ls", false, http).await?;
 71        let version_name = release
 72            .name
 73            .strip_prefix("Release ")
 74            .context("Elixir-ls release name does not start with prefix")?
 75            .to_owned();
 76
 77        let asset_name = format!("elixir-ls-{}.zip", &version_name);
 78        let asset = release
 79            .assets
 80            .iter()
 81            .find(|asset| asset.name == asset_name)
 82            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
 83
 84        let version = GitHubLspBinaryVersion {
 85            name: version_name,
 86            url: asset.browser_download_url.clone(),
 87        };
 88        Ok(Box::new(version) as Box<_>)
 89    }
 90
 91    async fn fetch_server_binary(
 92        &self,
 93        version: Box<dyn 'static + Send + Any>,
 94        container_dir: PathBuf,
 95        delegate: &dyn LspAdapterDelegate,
 96    ) -> Result<LanguageServerBinary> {
 97        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
 98        let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
 99        let version_dir = container_dir.join(format!("elixir-ls_{}", version.name));
100        let binary_path = version_dir.join("language_server.sh");
101
102        if fs::metadata(&binary_path).await.is_err() {
103            let mut response = delegate
104                .http_client()
105                .get(&version.url, Default::default(), true)
106                .await
107                .context("error downloading release")?;
108            let mut file = File::create(&zip_path)
109                .await
110                .with_context(|| format!("failed to create file {}", zip_path.display()))?;
111            if !response.status().is_success() {
112                Err(anyhow!(
113                    "download failed with status {}",
114                    response.status().to_string()
115                ))?;
116            }
117            futures::io::copy(response.body_mut(), &mut file).await?;
118
119            fs::create_dir_all(&version_dir)
120                .await
121                .with_context(|| format!("failed to create directory {}", version_dir.display()))?;
122            let unzip_status = smol::process::Command::new("unzip")
123                .arg(&zip_path)
124                .arg("-d")
125                .arg(&version_dir)
126                .output()
127                .await?
128                .status;
129            if !unzip_status.success() {
130                Err(anyhow!("failed to unzip elixir-ls archive"))?;
131            }
132
133            remove_matching(&container_dir, |entry| entry != version_dir).await;
134        }
135
136        Ok(LanguageServerBinary {
137            path: binary_path,
138            arguments: vec![],
139        })
140    }
141
142    async fn cached_server_binary(
143        &self,
144        container_dir: PathBuf,
145        _: &dyn LspAdapterDelegate,
146    ) -> Option<LanguageServerBinary> {
147        get_cached_server_binary(container_dir).await
148    }
149
150    async fn installation_test_binary(
151        &self,
152        container_dir: PathBuf,
153    ) -> Option<LanguageServerBinary> {
154        get_cached_server_binary(container_dir).await
155    }
156
157    async fn label_for_completion(
158        &self,
159        completion: &lsp::CompletionItem,
160        language: &Arc<Language>,
161    ) -> Option<CodeLabel> {
162        match completion.kind.zip(completion.detail.as_ref()) {
163            Some((_, detail)) if detail.starts_with("(function)") => {
164                let text = detail.strip_prefix("(function) ")?;
165                let filter_range = 0..text.find('(').unwrap_or(text.len());
166                let source = Rope::from(format!("def {text}").as_str());
167                let runs = language.highlight_text(&source, 4..4 + text.len());
168                return Some(CodeLabel {
169                    text: text.to_string(),
170                    runs,
171                    filter_range,
172                });
173            }
174            Some((_, detail)) if detail.starts_with("(macro)") => {
175                let text = detail.strip_prefix("(macro) ")?;
176                let filter_range = 0..text.find('(').unwrap_or(text.len());
177                let source = Rope::from(format!("defmacro {text}").as_str());
178                let runs = language.highlight_text(&source, 9..9 + text.len());
179                return Some(CodeLabel {
180                    text: text.to_string(),
181                    runs,
182                    filter_range,
183                });
184            }
185            Some((
186                CompletionItemKind::CLASS
187                | CompletionItemKind::MODULE
188                | CompletionItemKind::INTERFACE
189                | CompletionItemKind::STRUCT,
190                _,
191            )) => {
192                let filter_range = 0..completion
193                    .label
194                    .find(" (")
195                    .unwrap_or(completion.label.len());
196                let text = &completion.label[filter_range.clone()];
197                let source = Rope::from(format!("defmodule {text}").as_str());
198                let runs = language.highlight_text(&source, 10..10 + text.len());
199                return Some(CodeLabel {
200                    text: completion.label.clone(),
201                    runs,
202                    filter_range,
203                });
204            }
205            _ => {}
206        }
207
208        None
209    }
210
211    async fn label_for_symbol(
212        &self,
213        name: &str,
214        kind: SymbolKind,
215        language: &Arc<Language>,
216    ) -> Option<CodeLabel> {
217        let (text, filter_range, display_range) = match kind {
218            SymbolKind::METHOD | SymbolKind::FUNCTION => {
219                let text = format!("def {}", name);
220                let filter_range = 4..4 + name.len();
221                let display_range = 0..filter_range.end;
222                (text, filter_range, display_range)
223            }
224            SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => {
225                let text = format!("defmodule {}", name);
226                let filter_range = 10..10 + name.len();
227                let display_range = 0..filter_range.end;
228                (text, filter_range, display_range)
229            }
230            _ => return None,
231        };
232
233        Some(CodeLabel {
234            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
235            text: text[display_range].to_string(),
236            filter_range,
237        })
238    }
239}
240
241async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
242    (|| async move {
243        let mut last = None;
244        let mut entries = fs::read_dir(&container_dir).await?;
245        while let Some(entry) = entries.next().await {
246            last = Some(entry?.path());
247        }
248        last.map(|path| LanguageServerBinary {
249            path,
250            arguments: vec![],
251        })
252        .ok_or_else(|| anyhow!("no cached binary"))
253    })()
254    .await
255    .log_err()
256}