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