elixir.rs

  1use anyhow::{anyhow, bail, Context, Result};
  2use async_trait::async_trait;
  3use futures::StreamExt;
  4use gpui::{AsyncAppContext, Task};
  5pub use language::*;
  6use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
  7use schemars::JsonSchema;
  8use serde_derive::{Deserialize, Serialize};
  9use settings::Settings;
 10use smol::fs::{self, File};
 11use std::{
 12    any::Any,
 13    env::consts,
 14    ops::Deref,
 15    path::PathBuf,
 16    sync::{
 17        atomic::{AtomicBool, Ordering::SeqCst},
 18        Arc,
 19    },
 20};
 21use util::{
 22    async_maybe,
 23    fs::remove_matching,
 24    github::{latest_github_release, GitHubLspBinaryVersion},
 25    ResultExt,
 26};
 27
 28#[derive(Clone, Serialize, Deserialize, JsonSchema)]
 29pub struct ElixirSettings {
 30    pub lsp: ElixirLspSetting,
 31}
 32
 33#[derive(Clone, Serialize, Deserialize, JsonSchema)]
 34#[serde(rename_all = "snake_case")]
 35pub enum ElixirLspSetting {
 36    ElixirLs,
 37    NextLs,
 38    Local {
 39        path: String,
 40        arguments: Vec<String>,
 41    },
 42}
 43
 44#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
 45pub struct ElixirSettingsContent {
 46    lsp: Option<ElixirLspSetting>,
 47}
 48
 49impl Settings for ElixirSettings {
 50    const KEY: Option<&'static str> = Some("elixir");
 51
 52    type FileContent = ElixirSettingsContent;
 53
 54    fn load(
 55        default_value: &Self::FileContent,
 56        user_values: &[&Self::FileContent],
 57        _: &mut gpui::AppContext,
 58    ) -> Result<Self>
 59    where
 60        Self: Sized,
 61    {
 62        Self::load_via_json_merge(default_value, user_values)
 63    }
 64}
 65
 66pub struct ElixirLspAdapter;
 67
 68#[async_trait]
 69impl LspAdapter for ElixirLspAdapter {
 70    async fn name(&self) -> LanguageServerName {
 71        LanguageServerName("elixir-ls".into())
 72    }
 73
 74    fn short_name(&self) -> &'static str {
 75        "elixir-ls"
 76    }
 77
 78    fn will_start_server(
 79        &self,
 80        delegate: &Arc<dyn LspAdapterDelegate>,
 81        cx: &mut AsyncAppContext,
 82    ) -> Option<Task<Result<()>>> {
 83        static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
 84
 85        const NOTIFICATION_MESSAGE: &str = "Could not run the elixir language server, `elixir-ls`, because `elixir` was not found.";
 86
 87        let delegate = delegate.clone();
 88        Some(cx.spawn(|cx| async move {
 89            let elixir_output = smol::process::Command::new("elixir")
 90                .args(["--version"])
 91                .output()
 92                .await;
 93            if elixir_output.is_err() {
 94                if DID_SHOW_NOTIFICATION
 95                    .compare_exchange(false, true, SeqCst, SeqCst)
 96                    .is_ok()
 97                {
 98                    cx.update(|cx| {
 99                        delegate.show_notification(NOTIFICATION_MESSAGE, cx);
100                    })?
101                }
102                return Err(anyhow!("cannot run elixir-ls"));
103            }
104
105            Ok(())
106        }))
107    }
108
109    async fn fetch_latest_server_version(
110        &self,
111        delegate: &dyn LspAdapterDelegate,
112    ) -> Result<Box<dyn 'static + Send + Any>> {
113        let http = delegate.http_client();
114        let release = latest_github_release("elixir-lsp/elixir-ls", false, http).await?;
115        let version_name = release
116            .name
117            .strip_prefix("Release ")
118            .context("Elixir-ls release name does not start with prefix")?
119            .to_owned();
120
121        let asset_name = format!("elixir-ls-{}.zip", &version_name);
122        let asset = release
123            .assets
124            .iter()
125            .find(|asset| asset.name == asset_name)
126            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
127
128        let version = GitHubLspBinaryVersion {
129            name: version_name,
130            url: asset.browser_download_url.clone(),
131        };
132        Ok(Box::new(version) as Box<_>)
133    }
134
135    async fn fetch_server_binary(
136        &self,
137        version: Box<dyn 'static + Send + Any>,
138        container_dir: PathBuf,
139        delegate: &dyn LspAdapterDelegate,
140    ) -> Result<LanguageServerBinary> {
141        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
142        let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
143        let folder_path = container_dir.join("elixir-ls");
144        let binary_path = folder_path.join("language_server.sh");
145
146        if fs::metadata(&binary_path).await.is_err() {
147            let mut response = delegate
148                .http_client()
149                .get(&version.url, Default::default(), true)
150                .await
151                .context("error downloading release")?;
152            let mut file = File::create(&zip_path)
153                .await
154                .with_context(|| format!("failed to create file {}", zip_path.display()))?;
155            if !response.status().is_success() {
156                Err(anyhow!(
157                    "download failed with status {}",
158                    response.status().to_string()
159                ))?;
160            }
161            futures::io::copy(response.body_mut(), &mut file).await?;
162
163            fs::create_dir_all(&folder_path)
164                .await
165                .with_context(|| format!("failed to create directory {}", folder_path.display()))?;
166            let unzip_status = smol::process::Command::new("unzip")
167                .arg(&zip_path)
168                .arg("-d")
169                .arg(&folder_path)
170                .output()
171                .await?
172                .status;
173            if !unzip_status.success() {
174                Err(anyhow!("failed to unzip elixir-ls archive"))?;
175            }
176
177            remove_matching(&container_dir, |entry| entry != folder_path).await;
178        }
179
180        Ok(LanguageServerBinary {
181            path: binary_path,
182            arguments: vec![],
183        })
184    }
185
186    async fn cached_server_binary(
187        &self,
188        container_dir: PathBuf,
189        _: &dyn LspAdapterDelegate,
190    ) -> Option<LanguageServerBinary> {
191        get_cached_server_binary_elixir_ls(container_dir).await
192    }
193
194    async fn installation_test_binary(
195        &self,
196        container_dir: PathBuf,
197    ) -> Option<LanguageServerBinary> {
198        get_cached_server_binary_elixir_ls(container_dir).await
199    }
200
201    async fn label_for_completion(
202        &self,
203        completion: &lsp::CompletionItem,
204        language: &Arc<Language>,
205    ) -> Option<CodeLabel> {
206        match completion.kind.zip(completion.detail.as_ref()) {
207            Some((_, detail)) if detail.starts_with("(function)") => {
208                let text = detail.strip_prefix("(function) ")?;
209                let filter_range = 0..text.find('(').unwrap_or(text.len());
210                let source = Rope::from(format!("def {text}").as_str());
211                let runs = language.highlight_text(&source, 4..4 + text.len());
212                return Some(CodeLabel {
213                    text: text.to_string(),
214                    runs,
215                    filter_range,
216                });
217            }
218            Some((_, detail)) if detail.starts_with("(macro)") => {
219                let text = detail.strip_prefix("(macro) ")?;
220                let filter_range = 0..text.find('(').unwrap_or(text.len());
221                let source = Rope::from(format!("defmacro {text}").as_str());
222                let runs = language.highlight_text(&source, 9..9 + text.len());
223                return Some(CodeLabel {
224                    text: text.to_string(),
225                    runs,
226                    filter_range,
227                });
228            }
229            Some((
230                CompletionItemKind::CLASS
231                | CompletionItemKind::MODULE
232                | CompletionItemKind::INTERFACE
233                | CompletionItemKind::STRUCT,
234                _,
235            )) => {
236                let filter_range = 0..completion
237                    .label
238                    .find(" (")
239                    .unwrap_or(completion.label.len());
240                let text = &completion.label[filter_range.clone()];
241                let source = Rope::from(format!("defmodule {text}").as_str());
242                let runs = language.highlight_text(&source, 10..10 + text.len());
243                return Some(CodeLabel {
244                    text: completion.label.clone(),
245                    runs,
246                    filter_range,
247                });
248            }
249            _ => {}
250        }
251
252        None
253    }
254
255    async fn label_for_symbol(
256        &self,
257        name: &str,
258        kind: SymbolKind,
259        language: &Arc<Language>,
260    ) -> Option<CodeLabel> {
261        let (text, filter_range, display_range) = match kind {
262            SymbolKind::METHOD | SymbolKind::FUNCTION => {
263                let text = format!("def {}", name);
264                let filter_range = 4..4 + name.len();
265                let display_range = 0..filter_range.end;
266                (text, filter_range, display_range)
267            }
268            SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => {
269                let text = format!("defmodule {}", name);
270                let filter_range = 10..10 + name.len();
271                let display_range = 0..filter_range.end;
272                (text, filter_range, display_range)
273            }
274            _ => return None,
275        };
276
277        Some(CodeLabel {
278            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
279            text: text[display_range].to_string(),
280            filter_range,
281        })
282    }
283}
284
285async fn get_cached_server_binary_elixir_ls(
286    container_dir: PathBuf,
287) -> Option<LanguageServerBinary> {
288    let server_path = container_dir.join("elixir-ls/language_server.sh");
289    if server_path.exists() {
290        Some(LanguageServerBinary {
291            path: server_path,
292            arguments: vec![],
293        })
294    } else {
295        log::error!("missing executable in directory {:?}", server_path);
296        None
297    }
298}
299
300pub struct NextLspAdapter;
301
302#[async_trait]
303impl LspAdapter for NextLspAdapter {
304    async fn name(&self) -> LanguageServerName {
305        LanguageServerName("next-ls".into())
306    }
307
308    fn short_name(&self) -> &'static str {
309        "next-ls"
310    }
311
312    async fn fetch_latest_server_version(
313        &self,
314        delegate: &dyn LspAdapterDelegate,
315    ) -> Result<Box<dyn 'static + Send + Any>> {
316        let release =
317            latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?;
318        let version = release.name.clone();
319        let platform = match consts::ARCH {
320            "x86_64" => "darwin_amd64",
321            "aarch64" => "darwin_arm64",
322            other => bail!("Running on unsupported platform: {other}"),
323        };
324        let asset_name = format!("next_ls_{}", platform);
325        let asset = release
326            .assets
327            .iter()
328            .find(|asset| asset.name == asset_name)
329            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
330        let version = GitHubLspBinaryVersion {
331            name: version,
332            url: asset.browser_download_url.clone(),
333        };
334        Ok(Box::new(version) as Box<_>)
335    }
336
337    async fn fetch_server_binary(
338        &self,
339        version: Box<dyn 'static + Send + Any>,
340        container_dir: PathBuf,
341        delegate: &dyn LspAdapterDelegate,
342    ) -> Result<LanguageServerBinary> {
343        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
344
345        let binary_path = container_dir.join("next-ls");
346
347        if fs::metadata(&binary_path).await.is_err() {
348            let mut response = delegate
349                .http_client()
350                .get(&version.url, Default::default(), true)
351                .await
352                .map_err(|err| anyhow!("error downloading release: {}", err))?;
353
354            let mut file = smol::fs::File::create(&binary_path).await?;
355            if !response.status().is_success() {
356                Err(anyhow!(
357                    "download failed with status {}",
358                    response.status().to_string()
359                ))?;
360            }
361            futures::io::copy(response.body_mut(), &mut file).await?;
362
363            fs::set_permissions(
364                &binary_path,
365                <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
366            )
367            .await?;
368        }
369
370        Ok(LanguageServerBinary {
371            path: binary_path,
372            arguments: vec!["--stdio".into()],
373        })
374    }
375
376    async fn cached_server_binary(
377        &self,
378        container_dir: PathBuf,
379        _: &dyn LspAdapterDelegate,
380    ) -> Option<LanguageServerBinary> {
381        get_cached_server_binary_next(container_dir)
382            .await
383            .map(|mut binary| {
384                binary.arguments = vec!["--stdio".into()];
385                binary
386            })
387    }
388
389    async fn installation_test_binary(
390        &self,
391        container_dir: PathBuf,
392    ) -> Option<LanguageServerBinary> {
393        get_cached_server_binary_next(container_dir)
394            .await
395            .map(|mut binary| {
396                binary.arguments = vec!["--help".into()];
397                binary
398            })
399    }
400
401    async fn label_for_completion(
402        &self,
403        completion: &lsp::CompletionItem,
404        language: &Arc<Language>,
405    ) -> Option<CodeLabel> {
406        label_for_completion_elixir(completion, language)
407    }
408
409    async fn label_for_symbol(
410        &self,
411        name: &str,
412        symbol_kind: SymbolKind,
413        language: &Arc<Language>,
414    ) -> Option<CodeLabel> {
415        label_for_symbol_elixir(name, symbol_kind, language)
416    }
417}
418
419async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option<LanguageServerBinary> {
420    async_maybe!({
421        let mut last_binary_path = None;
422        let mut entries = fs::read_dir(&container_dir).await?;
423        while let Some(entry) = entries.next().await {
424            let entry = entry?;
425            if entry.file_type().await?.is_file()
426                && entry
427                    .file_name()
428                    .to_str()
429                    .map_or(false, |name| name == "next-ls")
430            {
431                last_binary_path = Some(entry.path());
432            }
433        }
434
435        if let Some(path) = last_binary_path {
436            Ok(LanguageServerBinary {
437                path,
438                arguments: Vec::new(),
439            })
440        } else {
441            Err(anyhow!("no cached binary"))
442        }
443    })
444    .await
445    .log_err()
446}
447
448pub struct LocalLspAdapter {
449    pub path: String,
450    pub arguments: Vec<String>,
451}
452
453#[async_trait]
454impl LspAdapter for LocalLspAdapter {
455    async fn name(&self) -> LanguageServerName {
456        LanguageServerName("local-ls".into())
457    }
458
459    fn short_name(&self) -> &'static str {
460        "local-ls"
461    }
462
463    async fn fetch_latest_server_version(
464        &self,
465        _: &dyn LspAdapterDelegate,
466    ) -> Result<Box<dyn 'static + Send + Any>> {
467        Ok(Box::new(()) as Box<_>)
468    }
469
470    async fn fetch_server_binary(
471        &self,
472        _: Box<dyn 'static + Send + Any>,
473        _: PathBuf,
474        _: &dyn LspAdapterDelegate,
475    ) -> Result<LanguageServerBinary> {
476        let path = shellexpand::full(&self.path)?;
477        Ok(LanguageServerBinary {
478            path: PathBuf::from(path.deref()),
479            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
480        })
481    }
482
483    async fn cached_server_binary(
484        &self,
485        _: PathBuf,
486        _: &dyn LspAdapterDelegate,
487    ) -> Option<LanguageServerBinary> {
488        let path = shellexpand::full(&self.path).ok()?;
489        Some(LanguageServerBinary {
490            path: PathBuf::from(path.deref()),
491            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
492        })
493    }
494
495    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
496        let path = shellexpand::full(&self.path).ok()?;
497        Some(LanguageServerBinary {
498            path: PathBuf::from(path.deref()),
499            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
500        })
501    }
502
503    async fn label_for_completion(
504        &self,
505        completion: &lsp::CompletionItem,
506        language: &Arc<Language>,
507    ) -> Option<CodeLabel> {
508        label_for_completion_elixir(completion, language)
509    }
510
511    async fn label_for_symbol(
512        &self,
513        name: &str,
514        symbol: SymbolKind,
515        language: &Arc<Language>,
516    ) -> Option<CodeLabel> {
517        label_for_symbol_elixir(name, symbol, language)
518    }
519}
520
521fn label_for_completion_elixir(
522    completion: &lsp::CompletionItem,
523    language: &Arc<Language>,
524) -> Option<CodeLabel> {
525    return Some(CodeLabel {
526        runs: language.highlight_text(&completion.label.clone().into(), 0..completion.label.len()),
527        text: completion.label.clone(),
528        filter_range: 0..completion.label.len(),
529    });
530}
531
532fn label_for_symbol_elixir(
533    name: &str,
534    _: SymbolKind,
535    language: &Arc<Language>,
536) -> Option<CodeLabel> {
537    Some(CodeLabel {
538        runs: language.highlight_text(&name.into(), 0..name.len()),
539        text: name.to_string(),
540        filter_range: 0..name.len(),
541    })
542}