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